68 Commits

Author SHA1 Message Date
47f95afabb Merge branch 'feat/codeToChat' into feat/ningDeShiDai 2026-03-10 20:53:31 +08:00
7fe87e515b feat:新增对话结束后添加结束语句 2026-03-10 20:53:13 +08:00
081ddec55c Merge branch 'feat/ningDeShiDai' of https://git.pengyejiatu.com/pengyejiatu/IC-Coder-Plugin into feat/ningDeShiDai 2026-03-10 19:18:06 +08:00
12c2f634bd Merge branch 'feat/codeToChat' into feat/ningDeShiDai 2026-03-10 19:05:45 +08:00
790110ba7e feat: 添加示例刷新按钮
- 在示例标题旁添加刷新按钮
   - 点击从13个示例中随机选择2个替换当前显示
   - 添加500ms节流防止频繁点击
   - 优化按钮交互动画效果
2026-03-10 19:04:45 +08:00
b9dc631bf7 Merge branch 'feat/ningDeShiDai' of https://git.pengyejiatu.com/pengyejiatu/Ic-coder-plugin into feat/ningDeShiDai 2026-03-10 19:01:38 +08:00
6425496d2e feat: 离线部署模式改进和 SystemVerilog 支持
- 添加离线模式仿真模拟:识别代码生成完成消息后自动模拟仿真流程
- 启用 iverilog SystemVerilog 2012 标准支持(-g2012)
- 优化进度条显示逻辑

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 18:56:53 +08:00
fd5a01c67f Merge branch 'feat/codeToChat' into feat/ningDeShiDai 2026-03-10 18:40:13 +08:00
29e80ce296 feat: 优化消息处理和界面显示
- 增强 messageHandler 消息处理逻辑
   - 优化 messageArea 显示效果
   - 改进 webviewContent 界面交互
2026-03-10 18:39:50 +08:00
c244a308d7 style:将spec改为specification 2026-03-10 17:08:36 +08:00
a25d68f527 feat: 添加用户手册只读预览功能
- 新增 UserManualPanel 组件实现只读预览
   - 支持 Markdown 完整渲染(表格、代码块、图片、分隔线)
   - 优化排版和字体大小
   - 用户无法编辑手册内容
2026-03-10 17:01:16 +08:00
77b54aebf0 feat: 添加用户手册功能
- 新增用户手册 Markdown 文档及配套截图
   - 新增打开用户手册命令
   - 在侧边栏和主面板中集成用户手册入口
   - 优化用户手册打开方式,支持 Markdown 预览
2026-03-10 16:29:37 +08:00
840436eb36 refactor: 优化设置面板和模型提示文案
- 移除设置面板中的规则配置标签页
   - 更新模型选择器提示文案为"FPGA专属微调模型"
2026-03-10 14:23:37 +08:00
f5dd7534f0 feat:将模型选中固定为max模型
- 并且鼠标移动上去显示“IC Coder自研顶尖微调模型“
2026-03-10 14:13:20 +08:00
ebb9de5294 tyle:修改了聊天面板的样式及删除了用户反馈和web端的跳转 2026-03-10 14:02:44 +08:00
531d140b99 refactor: 优化错误提示信息
- 将"当前访问人数过多"改为更通用的"处理用户消息失败"
   - 移除错误消息中的  emoji,保持简洁
2026-03-09 15:55:51 +08:00
97b8e8aa7d refactor: 更新后端服务地址占位符 2026-03-09 15:34:41 +08:00
4ed998e937 feat: 添加后端服务地址自定义配置功能
- 在设置面板添加后端服务地址配置项
   - 支持保存、加载和重置自定义配置
   - 配置持久化存储,重启后保留
   - 添加 SSE 请求日志用于验证配置
2026-03-09 15:29:56 +08:00
ad0f0336d5 feat: 移除用户信息、余额检查和登录过期提示
- 隐藏用户信息显示和退出登录按钮
   - 删除发送消息前的余额检查逻辑
   - 删除对话完成后的余额更新逻辑
   - 注释掉所有登录过期弹窗提示
   - 移除用户服务和余额服务的初始化调用
2026-03-09 14:28:41 +08:00
7cde4fa138 refactor: 优化代码格式和用户提示
- 统一代码格式化(Prettier)
- 将 iverilog 相关错误提示改为 'IC Coder编译器'
- 优化后端服务错误提示为 '当前访问人数过多,请稍后重试'
- 修复代码风格一致性问题
2026-03-09 11:10:56 +08:00
1b7259d1c1 feat:排除打包项目中的waveform_trace文件中的无关文档 2026-03-09 10:41:17 +08:00
09ff812562 feat:修复 Windows vvp 解析问题
- 修复 iverilog 生成的 .vvp 文件 shebang 导致 Windows 解析失败
2026-03-07 18:41:42 +08:00
e7c631d532 feat: 优化文档结构
- 将文档移至 docs/ 目录统一管理
   - 更新 .vscodeignore 排除规则
2026-03-07 18:41:14 +08:00
06573e37d7 feat: 优化 webpack 打包配置
- 添加自动模式切换(开发/生产)
   - 启用 Tree Shaking 移除未使用代码
   - 加快编译速度(transpileOnly)
   - 添加打包体积监控
   - 自动清理旧文件
   - 添加打包优化文档
2026-03-06 18:27:56 +08:00
d740f4da44 feat: 支持文件路径标签带行号点击跳转
- 前端解析 file.v:1-2 格式,提取文件名和行号
   - 新增 openFilePathTag 命令,支持智能文件查找
   - 修复模板字符串中正则表达式转义问题
   - 不影响现有 openFile 和 diff 功能
2026-03-06 16:24:21 +08:00
f24bd38ec7 feat: 优化上下文项显示和识别逻辑
- 支持显示所有类型的上下文项(文件和代码片段)
   - 增强路径识别,支持代码片段格式(文件名:行号-行号)
2026-03-06 15:40:59 +08:00
45934baf0a feat: 添加上下文项点击功能
- 文件类型可点击打开文件
   - 代码片段可点击打开文件并选中对应代码
   - 文件夹类型不可点击
2026-03-06 10:13:27 +08:00
4384ee53c5 fix: 修复关闭面板后快捷键无法自动打开面板的问题
- 通过 try-catch 检测 webview 是否真正可用
   - 修复 panel._isDisposed 检测不准确的问题
   - 增加异常捕获防止发送消息时崩溃
   - 延长消息发送延迟至 500ms 确保面板加载完成
2026-03-06 10:05:52 +08:00
d89c326be5 Merge branch 'feat/DeleteConfirmation' into feat/codeToChat 2026-03-06 09:16:12 +08:00
2dccb4f871 update:changelog.md 2026-03-06 09:11:33 +08:00
a9ddf3074e 1.0.12 2026-03-06 09:10:51 +08:00
db087bb184 update:更新changelog.md 2026-03-06 09:10:09 +08:00
5e9083041f fix: 修复多选问题提交后选中项不显示高亮的问题 2026-03-06 09:08:38 +08:00
be0555d6bc feat:codeToChat 2026-03-06 08:59:02 +08:00
ea19dfcbe6 fix: 修复 waveform_trace 工具执行失败和类型错误
- 修复 waveform_trace 工具因 stderr 输出导致的误判失败
   - 修复 messageHandler onQuestion 回调的类型签名错误
2026-03-05 17:25:29 +08:00
fa55e32153 feat: 支持 AskUserQuestion 多问题和多选功能
- 新增 QuestionItem 类型支持单个问题配置(question/options/multiSelect)
   - AskUserEvent 改为 questions 数组支持多问题
   - AnswerRequest 新增 answers 字段支持多问题答案提交
   - 前端渲染支持单选按钮(radio)和多选复选框(checkbox)
   - 答案格式:{\"0\": [\"选项1\"], \"1\": [\"选项A\", \"选项B\"]}
   - 保持向后兼容旧的单问题格式
2026-03-05 16:58:59 +08:00
f6b1f5c45a 1.0.11 2026-03-05 10:39:30 +08:00
1f9a1822c9 fix: 修复打包后图片资源无法加载的问题
- 配置 webpack copy-webpack-plugin 将 src/assets 复制到 dist/assets
- 更新所有图片引用路径从 src/assets 改为 dist/assets
- 修改 localResourceRoots 配置以允许访问 dist/assets
2026-03-05 10:38:40 +08:00
63015c6bbc fix:排除 PUBLISH.md,避免敏感信息被打包 2026-03-05 09:34:44 +08:00
24b30df992 fix: 排除 docs 目录避免中文文件名打包冲突 2026-03-05 09:24:27 +08:00
67b1003831 style:将宁德时代欢迎弹窗换成全企业的 2026-03-04 22:19:14 +08:00
00d37bdaf0 1.0.9 2026-03-04 21:08:08 +08:00
c5fcb1427e Update:README.md 2026-03-04 21:07:50 +08:00
9118ebd662 feat:企业欢迎弹窗优化 2026-03-04 18:58:18 +08:00
19cdf47bed style: 将工具折叠图标颜色从蓝色改为灰色
- 修改 toolIcons.ts 中的 SVG 填充色为 #8a8a8a
   - 清理 messageArea.ts 中冗余的 CSS 样式规则
2026-03-04 16:46:40 +08:00
95bac94479 fix:修改代码变更继续对话查找不到之前的代码变更信息的bug 2026-03-04 16:17:56 +08:00
421a8934a7 chore: 优化打包配置,排除重复的 exe 文件
- 添加 .vscodeignore 排除 tools/waveform_trace/src/dist
   - 移除 package.json 的 files 字段
   - 减小 .vsix 打包体积
2026-03-04 15:11:46 +08:00
f7f45668d3 style: 统一使用蓝色主题色
- 压缩图标改为蓝色 #007ACC
- 问题选项按钮改为蓝色背景,悬停深蓝色
- 按钮、进度条等组件统一使用蓝色主题
- 添加 CSS 强制规则确保图标在所有主题下显示蓝色
2026-03-04 14:51:36 +08:00
c8e9a5b897 fix: 修复继续对话时消息覆盖问题并添加波形追踪工具
- 修复继续对话时 AI 消息被覆盖的问题
- 用户发送新消息时重置分段消息容器
- 继续对话时复用未完成的消息容器
- 添加 waveform_trace.exe 工具到仓库
- 更新 .gitignore 规则
2026-03-04 11:43:45 +08:00
a1bfa62796 feat:Update CHANGE.md 2026-03-03 20:21:37 +08:00
64e11cbc3c 1.0.8 2026-03-03 20:19:46 +08:00
15445aa13c fix: 修复继续对话时消息覆盖问题
- 对话完成时正确重置 currentSegmentedMessage
- 继续对话时创建新的消息容器
- 删除调试日志代码
2026-03-03 20:04:41 +08:00
52834047f2 feat: 每次登录都显示试用用户欢迎弹窗
- 移除 hasWelcomed 标记,不再记录是否已显示
- 试用用户每次打开聊天面板都会看到欢迎弹窗
2026-03-03 19:19:37 +08:00
76817675f1 fix: 修复试用用户欢迎弹窗不显示的问题
- 修复 userService 中 null 值未正确赋值的问题
- 优化欢迎弹窗判断逻辑:null=长期有效,undefined=无效
- 添加测试命令 resetWelcomeModal 用于清除弹窗标记
2026-03-03 19:06:02 +08:00
2cce8f94c9 fix: 修复试用用户无过期时间时欢迎弹窗不显示的问题
- 优化欢迎弹窗判断逻辑,支持无过期时间的长期试用用户
   - 只有在有过期时间且已过期时才不显示欢迎弹窗
   - 改进日志输出,更清晰地显示判断流
2026-03-03 18:48:30 +08:00
9b5f102d9f feat: 完善企业试用用户欢迎弹窗逻辑
- 添加试用到期时间检查

- 仅在试用未过期且有到期时间时显示欢迎弹窗

- 试用过期或无到期时间则显示邀请码弹窗

- 修复窗口重载后弹窗标记丢失问题
2026-03-03 18:26:27 +08:00
68de33165e fix:修复企业试用用户仍弹出邀请码的问题 2026-03-03 18:03:43 +08:00
f56ad33366 feat:实现删除文件确认功能 2026-03-03 17:08:59 +08:00
35c63802b5 feat:添加文件路径标签显示和rules需求文档 2026-03-03 16:45:23 +08:00
3458f6fe23 fix:解决登录过期点击重新登录失败的bug 2026-03-03 14:19:00 +08:00
8f305033f7 release: 1.0.7
- 修复 AI 响应内容重复显示问题
2026-03-02 19:37:16 +08:00
4a18f1c418 1.0.7 2026-03-02 19:29:03 +08:00
373edb6d80 fix: 修复 AI 响应内容重复显示问题
- 完成标记不再重复发送 segments,避免内容在前端重复显示
   - 移除调试日志
2026-03-02 19:25:25 +08:00
1c66e0e599 feat:更新CHANGELOG 2026-03-02 17:46:09 +08:00
536e7720cb 1.0.6 2026-03-02 17:36:40 +08:00
75eac4b1ce feat:删除文件确认功能实现文档 2026-03-02 17:36:20 +08:00
9ed0afee6b feat:解决添加上下文搜索选择文件不匹配的问题 2026-03-02 15:43:33 +08:00
f700473967 fix: clear expired auth state before relogin 2026-03-02 14:51:11 +08:00
61 changed files with 4179 additions and 1561 deletions

3
.gitignore vendored
View File

@ -4,8 +4,7 @@ node_modules
.vscode-test/
*.vsix
# waveform_trace 打包产物exe 太大,通过 Release 发布)
tools/waveform_trace/bin/
# waveform_trace 打包产物
tools/waveform_trace/src/build/
tools/waveform_trace/src/dist/
tools/waveform_trace/src/*.spec

33
.vscodeignore Normal file
View File

@ -0,0 +1,33 @@
# 开发文件
.vscode/**
.vscode-test/**
src/**
.gitignore
.yarnrc
vsc-extension-quickstart.md
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts
# 测试文件
out/test/**
# 依赖
node_modules/**
# 文档(避免中文文件名打包问题)
docs/**
CLAUDE.md
# 只排除 waveform_trace 的 src/dist 目录
tools/waveform_trace/src/**
tools/iverilog/examples/**
tools/iverilog/INSTALL.md
tools/iverilog/README.md
tools/iverilog/DOWNLOAD_INSTRUCTIONS.md
# Git 相关
.git/**
.github/**

View File

@ -2,6 +2,57 @@
所有重要的项目变更都将记录在此文件中。
## [1.0.12] - 2026-03-06
### 新增
- 支持 AskUserQuestion 多问题和多选功能
## [1.0.9] - 2026-03-04
### 优化
- 将工具折叠图标颜色从蓝色改为灰色
- 统一使用蓝色主题色
- 优化打包配置,排除重复的 exe 文件
### 修复
- 修复代码变更继续对话查找不到之前的代码变更信息的 bug
- 修复对话展示两遍的问题
## [1.0.8] - 2026-03-03
### 新增
- 删除文件确认功能
- 文件路径标签显示
- 企业试用用户欢迎弹窗优化
### 修复
- 修复继续对话时消息覆盖问题
- 修复试用用户欢迎弹窗显示逻辑
- 修复企业试用用户仍弹出邀请码的问题
- 修复登录过期点击重新登录失败的问题
## [1.0.7] - 2026-03-02
### 修复
- 修复 AI 响应内容重复显示问题
## [1.0.6] - 2026-03-02
### 新增
- Git Diff 功能:支持查看当前文件的 Git 差异对比
### 修复
- 修复添加上下文搜索选择文件不匹配的问题
- 修复过期认证状态未清除导致重新登录失败的问题
## [1.0.4] - 2026-01-28
IC Coder插件端正式上线。

View File

@ -0,0 +1,261 @@
# AskUserQuestion 多选支持 - API 设计文档
## 问题描述
当前 AI 询问用户问题时存在以下问题:
1. 后端返回的选项不准确
2. 多个问题只给几个选项
3. 不支持多选方式
## 需求
实现一个问题对应多个选项,支持多选的方式。
## 数据结构设计
### 后端返回格式
后端通过 SSE 的 `ask_user` 事件返回以下格式:
```json
{
"askId": "ask_1234567890",
"questions": [
{
"question": "请确认 SPI 控制器的配置需求:工作模式?",
"options": [
"Master/8位/模式0/固定分频/需要CS",
"Master/可配置位宽/可配置模式/需要CS",
"Slave模式"
],
"multiSelect": false
},
{
"question": "数据位宽?",
"options": [
"8位 还是其他?"
],
"multiSelect": false
},
{
"question": "时钟极性和相位?",
"options": [
"CPOL=0/CPHA=0 (模式0) 还是其他模式?"
],
"multiSelect": false
},
{
"question": "时钟分频?",
"options": [
"需要可配置的分频比吗?"
],
"multiSelect": false
},
{
"question": "是否需要芯片选信号 (CS) 控制?",
"options": [
"是",
"否"
],
"multiSelect": false
}
]
}
```
### 前端数据结构
#### 1. API 类型定义 (`src/types/api.ts`)
```typescript
/** ask_user 事件数据 */
export interface AskUserEvent {
askId: string;
questions: QuestionItem[];
}
/** 单个问题项 */
export interface QuestionItem {
question: string;
options: string[];
multiSelect?: boolean; // 是否支持多选,默认 false
}
```
#### 2. MessageSegment 类型 (`src/services/dialogService.ts`)
```typescript
export interface MessageSegment {
type: "text" | "tool" | "question" | "agent" | "plan" | "progress";
// ... 其他字段
askId?: string;
questions?: QuestionItem[]; // 改为问题数组
}
```
#### 3. 用户回答格式 (`src/types/api.ts`)
```typescript
export interface AnswerRequest {
taskId: string;
askId: string;
answers: {
[questionIndex: number]: string[]; // 每个问题的答案数组(支持多选)
};
}
```
## 前端实现要点
### 1. 显示多个问题
```typescript
// 遍历 questions 数组,为每个问题生成 UI
segment.questions?.forEach((q, index) => {
// 显示问题标题
// 显示选项(单选或多选)
// 收集答案
});
```
### 2. 多选支持
```typescript
if (q.multiSelect) {
// 渲染复选框
// 允许选择多个选项
} else {
// 渲染单选按钮
// 只允许选择一个选项
}
```
### 3. 提交答案
```typescript
const answers = {
0: ["Master/8位/模式0/固定分频/需要CS"], // 第1个问题的答案
1: ["8位 还是其他?"], // 第2个问题的答案
2: ["CPOL=0/CPHA=0 (模式0) 还是其他模式?"], // 第3个问题的答案
// ...
};
vscode.postMessage({
command: 'userAnswer',
askId: 'ask_1234567890',
answers: answers
});
```
## 后端需要做的修改
### 1. 修改 AskUserQuestion 工具的返回格式
从:
```json
{
"askId": "xxx",
"question": "单个问题",
"options": ["选项1", "选项2"]
}
```
改为:
```json
{
"askId": "xxx",
"questions": [
{
"question": "问题1",
"options": ["选项1", "选项2"],
"multiSelect": false
},
{
"question": "问题2",
"options": ["选项A", "选项B", "选项C"],
"multiSelect": true
}
]
}
```
### 2. 接收答案的格式
从:
```json
{
"taskId": "xxx",
"askId": "xxx",
"selected": ["选项1"],
"customInput": "自定义输入"
}
```
改为:
```json
{
"taskId": "xxx",
"askId": "xxx",
"answers": {
"0": ["选项1"], // 第1个问题的答案
"1": ["选项A", "选项B"] // 第2个问题的答案多选
}
}
```
## 示例场景
### 场景SPI 控制器配置
**后端发送:**
```json
{
"askId": "ask_spi_config",
"questions": [
{
"question": "工作模式?",
"options": [
"Master/8位/模式0/固定分频/需要CS",
"Master/可配置位宽/可配置模式/需要CS",
"Slave模式"
],
"multiSelect": false
},
{
"question": "需要哪些功能?",
"options": [
"可配置时钟分频",
"可配置数据位宽",
"支持多个CS",
"DMA支持"
],
"multiSelect": true
}
]
}
```
**用户选择:**
- 问题1选择 "Master/8位/模式0/固定分频/需要CS"
- 问题2选择 "可配置时钟分频" 和 "可配置数据位宽"
**前端提交:**
```json
{
"taskId": "task_xxx",
"askId": "ask_spi_config",
"answers": {
"0": ["Master/8位/模式0/固定分频/需要CS"],
"1": ["可配置时钟分频", "可配置数据位宽"]
}
}
```
## 总结
这个设计方案:
1. ✅ 支持多个问题
2. ✅ 每个问题有多个选项
3. ✅ 支持单选和多选
4. ✅ 数据结构清晰,易于扩展
5. ✅ 向后兼容(可以只有一个问题)

View File

@ -88,6 +88,7 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
5. 点击 **Create** 完成创建
**注意事项:**
- Publisher ID 一旦创建无法修改
- Publisher ID 必须全局唯一
- 建议使用有意义且专业的 ID
@ -126,6 +127,7 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
## [0.0.2] - 2025-12-29
### 新增
- 添加发送和暂停按钮功能
- 添加一键优化按钮组件
- 添加 Plan 开关组件
@ -133,11 +135,13 @@ CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDO
- 添加上下文压缩功能
### 改进
- 优化用户界面交互体验
## [0.0.1] - 2025-12-XX
### 新增
- 初始版本发布
- Verilog 代码智能生成
- 集成 iverilog 仿真工具
@ -161,6 +165,7 @@ in the Software without restriction...
### 4. 优化 README.md
确保 README 包含:
- 清晰的功能介绍
- 使用截图或 GIF 演示
- 详细的使用说明
@ -219,6 +224,7 @@ pnpm vsce publish
**步骤:**
1. 本地打包插件:
```bash
pnpm run package
pnpm vsce package[pnpm vsce package --no-dependencies]
@ -257,7 +263,7 @@ pnpm vsce publish major
```bash
# 发布指定版本
pnpm vsce publish 0.0.3
npx vsce publish --packagePath iccoder-1.0.7.vsix
```
### 更新流程建议
@ -268,8 +274,6 @@ pnpm vsce publish 0.0.3
4. 执行发布命令
5. 验证市场上的插件是否正常
## 更新流程
1. 修改版本号
@ -302,10 +306,7 @@ pnpm vsce publish 0.0.3
pnpm vsce package --no-dependencies
```
3. 手动上传/命令上传
- https://marketplace.visualstudio.com/ 在这个里面手动上传 更新就选择update
- 命令上传vsce publish
@ -318,6 +319,7 @@ pnpm vsce publish 0.0.3
**原因:** PAT Token 无效或过期
**解决方案:**
- 重新生成 PAT Token
- 重新登录:`pnpm vsce login ic-coder-team`
@ -326,6 +328,7 @@ pnpm vsce publish 0.0.3
**原因:** Publisher ID 不存在或不匹配
**解决方案:**
- 检查 `package.json` 中的 `publisher` 字段
- 确认已在市场创建对应的 Publisher
@ -334,17 +337,20 @@ pnpm vsce publish 0.0.3
**原因:** 必需文件缺失
**解决方案:**
- 确保 `dist/` 目录存在且包含编译后的代码
- 运行 `pnpm run package` 重新构建
### 4. 插件审核被拒
**常见原因:**
- 插件名称或描述违反市场规则
- 图标不符合要求(建议 128x128 PNG
- README 内容不完整
**解决方案:**
- 查看审核反馈邮件
- 修改相关内容后重新发布
@ -366,6 +372,7 @@ code --install-extension ic-coder-plugin-0.0.2.vsix
```
或者在 VS Code 中:
1. 打开扩展面板
2. 点击 `...` 菜单
3. 选择 **Install from VSIX...**

View File

@ -0,0 +1,804 @@
# VS Code Extension API 核心知识点
## 目录
- [1. Extension 生命周期](#1-extension-生命周期) ⭐⭐⭐
- [2. 激活事件 (Activation Events)](#2-激活事件-activation-events) ⭐⭐
- [3. 命令系统 (Commands)](#3-命令系统-commands) ⭐⭐
- [4. Webview API](#4-webview-api) ⭐⭐⭐⭐⭐ **面试重点**
- [5. TreeView 和自定义视图](#5-treeview-和自定义视图) ⭐⭐
- [6. 文件系统操作](#6-文件系统操作) ⭐⭐⭐
- [7. 配置和存储](#7-配置和存储) ⭐⭐⭐⭐ **面试重点**
- [8. 消息通知](#8-消息通知) ⭐
- [9. 语言特性支持](#9-语言特性支持) ⭐
- [10. 调试和诊断](#10-调试和诊断) ⭐
---
## 1. Extension 生命周期 ⭐⭐⭐
### 1.1 核心函数 🔥必考
```typescript
// extension.ts
import * as vscode from 'vscode';
// 插件激活时调用(只调用一次)
export function activate(context: vscode.ExtensionContext) {
console.log('Extension is now active!');
// 注册命令、视图、事件监听等
// 使用 context.subscriptions 管理资源
}
// 插件停用时调用(清理资源)
export function deactivate() {
console.log('Extension is deactivated');
// 清理资源、关闭连接等
}
```
### 1.2 ExtensionContext 重要属性 🔥必考
```typescript
interface ExtensionContext {
// 插件订阅管理(自动清理)
subscriptions: { dispose(): any }[];
// 工作区存储路径
storageUri: vscode.Uri | undefined;
globalStorageUri: vscode.Uri;
// 插件路径
extensionUri: vscode.Uri;
extensionPath: string;
// 状态存储
workspaceState: Memento; // 工作区级别
globalState: Memento; // 全局级别
secrets: SecretStorage; // 敏感信息存储
// 环境变量
environmentVariableCollection: EnvironmentVariableCollection;
}
```
### 1.3 资源管理最佳实践 🔥必考
```typescript
export function activate(context: vscode.ExtensionContext) {
// ✅ 推荐:使用 context.subscriptions 自动管理
context.subscriptions.push(
vscode.commands.registerCommand('extension.command', () => {})
);
// ❌ 不推荐:手动管理容易忘记清理
const disposable = vscode.commands.registerCommand('extension.command', () => {});
// 需要在 deactivate 中手动调用 disposable.dispose()
}
```
---
## 2. 激活事件 (Activation Events) ⭐⭐
### 2.1 常用激活事件 📌重要
```json
// package.json
{
"activationEvents": [
// 启动时激活
"onStartupFinished",
// 执行命令时激活
"onCommand:extension.helloWorld",
// 打开特定语言文件时激活
"onLanguage:javascript",
"onLanguage:verilog",
// 打开特定文件类型时激活
"onFileSystem:sftp",
// 打开特定视图时激活
"onView:myCustomView",
// 调试时激活
"onDebug",
// 打开特定 URI 时激活
"onUri",
// Webview 恢复时激活
"onWebviewPanel:myWebview",
// 任务执行时激活
"onTaskType:npm"
]
}
```
### 2.2 延迟激活策略 🔥必考
```typescript
// ✅ 推荐:使用 onStartupFinished 延迟激活
"activationEvents": ["onStartupFinished"]
// ❌ 不推荐:使用 * 会拖慢启动速度
"activationEvents": ["*"]
```
---
## 3. 命令系统 (Commands)
### 3.1 注册命令
```typescript
// 注册简单命令
const disposable = vscode.commands.registerCommand(
'extension.helloWorld',
() => {
vscode.window.showInformationMessage('Hello World!');
}
);
context.subscriptions.push(disposable);
// 注册带参数的命令
vscode.commands.registerCommand(
'extension.openFile',
(filePath: string) => {
vscode.workspace.openTextDocument(filePath).then(doc => {
vscode.window.showTextDocument(doc);
});
}
);
```
### 3.2 执行命令
```typescript
// 执行内置命令
await vscode.commands.executeCommand('workbench.action.files.save');
// 执行自定义命令
await vscode.commands.executeCommand('extension.openFile', '/path/to/file');
// 获取所有可用命令
const commands = await vscode.commands.getCommands();
```
### 3.3 常用内置命令
```typescript
// 文件操作
'workbench.action.files.save'
'workbench.action.files.saveAll'
'workbench.action.closeActiveEditor'
// 编辑器操作
'editor.action.formatDocument'
'editor.action.commentLine'
'editor.action.selectAll'
// 窗口操作
'workbench.action.toggleSidebarVisibility'
'workbench.action.terminal.new'
'workbench.action.quickOpen'
// Git 操作
'git.commit'
'git.push'
'git.pull'
```
---
## 4. Webview API ⭐⭐⭐⭐⭐ **面试重点**
### 4.1 创建 Webview Panel 🔥必考
```typescript
const panel = vscode.window.createWebviewPanel(
'myWebview', // viewType唯一标识
'My Webview', // 标题
vscode.ViewColumn.One, // 显示位置
{
enableScripts: true, // 启用 JavaScript
retainContextWhenHidden: true, // 隐藏时保留状态
localResourceRoots: [ // 允许访问的本地资源路径
vscode.Uri.joinPath(context.extensionUri, 'media')
]
}
);
```
### 4.2 设置 Webview 内容
```typescript
panel.webview.html = getWebviewContent();
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Webview</title>
</head>
<body>
<h1>Hello from Webview!</h1>
<button onclick="sendMessage()">Send Message</button>
<script>
const vscode = acquireVsCodeApi();
function sendMessage() {
vscode.postMessage({
command: 'alert',
text: 'Hello from Webview!'
});
}
// 接收来自 Extension 的消息
window.addEventListener('message', event => {
const message = event.data;
console.log('Received:', message);
});
</script>
</body>
</html>`;
}
```
### 4.3 Webview 消息通信 🔥必考(项目核心)
```typescript
// Extension → Webview
panel.webview.postMessage({
command: 'update',
data: 'some data'
});
// Webview → Extension
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'alert':
vscode.window.showInformationMessage(message.text);
break;
case 'getData':
// 处理数据请求
panel.webview.postMessage({
command: 'dataResponse',
data: fetchData()
});
break;
}
},
undefined,
context.subscriptions
);
```
### 4.4 Webview 生命周期管理 📌重要
```typescript
// 监听 Webview 关闭事件
panel.onDidDispose(
() => {
// 清理资源
console.log('Webview disposed');
},
null,
context.subscriptions
);
// 监听 Webview 可见性变化
panel.onDidChangeViewState(
e => {
if (e.webviewPanel.visible) {
console.log('Webview is now visible');
}
},
null,
context.subscriptions
);
```
### 4.5 加载本地资源 📌重要
```typescript
// 获取本地资源 URI
const scriptUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, 'media', 'script.js')
);
const styleUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, 'media', 'style.css')
);
// 在 HTML 中使用
const html = `
<link href="${styleUri}" rel="stylesheet">
<script src="${scriptUri}"></script>
`;
```
### 4.6 Webview 状态持久化 📌重要
```typescript
// Webview 中保存状态
const vscode = acquireVsCodeApi();
const state = vscode.getState() || { count: 0 };
// 更新状态
state.count++;
vscode.setState(state);
// Extension 中序列化状态
panel.webview.options = {
enableScripts: true,
retainContextWhenHidden: true
};
// 恢复 Webview
vscode.window.registerWebviewPanelSerializer('myWebview', {
async deserializeWebviewPanel(webviewPanel, state) {
webviewPanel.webview.html = getWebviewContent();
// 恢复状态
webviewPanel.webview.postMessage({ command: 'restore', state });
}
});
```
---
## 5. TreeView 和自定义视图
### 5.1 创建 TreeView Provider
```typescript
class MyTreeDataProvider implements vscode.TreeDataProvider<TreeItem> {
private _onDidChangeTreeData = new vscode.EventEmitter<TreeItem | undefined>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
refresh(): void {
this._onDidChangeTreeData.fire(undefined);
}
getTreeItem(element: TreeItem): vscode.TreeItem {
return element;
}
getChildren(element?: TreeItem): Thenable<TreeItem[]> {
if (!element) {
// 返回根节点
return Promise.resolve([
new TreeItem('Item 1', vscode.TreeItemCollapsibleState.None),
new TreeItem('Item 2', vscode.TreeItemCollapsibleState.Collapsed)
]);
}
// 返回子节点
return Promise.resolve([]);
}
}
class TreeItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly collapsibleState: vscode.TreeItemCollapsibleState
) {
super(label, collapsibleState);
this.tooltip = `Tooltip for ${label}`;
this.command = {
command: 'extension.itemClicked',
title: 'Click Item',
arguments: [this]
};
}
}
```
### 5.2 注册 TreeView
```typescript
const treeDataProvider = new MyTreeDataProvider();
const treeView = vscode.window.createTreeView('myTreeView', {
treeDataProvider,
showCollapseAll: true
});
context.subscriptions.push(treeView);
// 刷新视图
treeDataProvider.refresh();
```
### 5.3 WebviewView Provider侧边栏 Webview
```typescript
class MyWebviewViewProvider implements vscode.WebviewViewProvider {
resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
token: vscode.CancellationToken
) {
webviewView.webview.options = {
enableScripts: true
};
webviewView.webview.html = getWebviewContent();
webviewView.webview.onDidReceiveMessage(message => {
// 处理消息
});
}
}
// 注册
vscode.window.registerWebviewViewProvider(
'myWebviewView',
new MyWebviewViewProvider()
);
```
---
## 6. 文件系统操作 ⭐⭐⭐
### 6.1 读取文件 📌重要
```typescript
// 读取文本文件
const uri = vscode.Uri.file('/path/to/file.txt');
const content = await vscode.workspace.fs.readFile(uri);
const text = Buffer.from(content).toString('utf8');
// 使用 TextDocument API
const document = await vscode.workspace.openTextDocument(uri);
const text = document.getText();
```
### 6.2 写入文件
```typescript
// 写入文件
const uri = vscode.Uri.file('/path/to/file.txt');
const content = Buffer.from('Hello World', 'utf8');
await vscode.workspace.fs.writeFile(uri, content);
// 使用 WorkspaceEdit
const edit = new vscode.WorkspaceEdit();
edit.createFile(uri, { overwrite: true });
edit.insert(uri, new vscode.Position(0, 0), 'Hello World');
await vscode.workspace.applyEdit(edit);
```
### 6.3 文件监听
```typescript
// 监听文件变化
const watcher = vscode.workspace.createFileSystemWatcher('**/*.js');
watcher.onDidCreate(uri => {
console.log('File created:', uri.fsPath);
});
watcher.onDidChange(uri => {
console.log('File changed:', uri.fsPath);
});
watcher.onDidDelete(uri => {
console.log('File deleted:', uri.fsPath);
});
context.subscriptions.push(watcher);
```
### 6.4 工作区操作
```typescript
// 获取工作区文件夹
const workspaceFolders = vscode.workspace.workspaceFolders;
if (workspaceFolders) {
const rootPath = workspaceFolders[0].uri.fsPath;
}
// 查找文件
const files = await vscode.workspace.findFiles(
'**/*.ts', // include pattern
'**/node_modules/**' // exclude pattern
);
// 打开文件
const document = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(document);
```
---
## 7. 配置和存储 ⭐⭐⭐⭐ **面试重点**
### 7.1 读取配置 📌重要
```typescript
// 读取配置
const config = vscode.workspace.getConfiguration('myExtension');
const value = config.get<string>('settingName', 'defaultValue');
// 监听配置变化
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('myExtension.settingName')) {
console.log('Configuration changed');
}
});
```
### 7.2 更新配置
```typescript
const config = vscode.workspace.getConfiguration('myExtension');
// 更新用户配置(全局)
await config.update('settingName', 'newValue', vscode.ConfigurationTarget.Global);
// 更新工作区配置
await config.update('settingName', 'newValue', vscode.ConfigurationTarget.Workspace);
```
### 7.3 状态存储 🔥必考
```typescript
// 工作区状态(仅当前工作区)
await context.workspaceState.update('key', 'value');
const value = context.workspaceState.get('key');
// 全局状态(跨工作区)
await context.globalState.update('key', 'value');
const value = context.globalState.get('key');
// 存储对象
await context.globalState.update('userData', { name: 'John', age: 30 });
```
### 7.4 敏感信息存储 🔥必考Token 管理)
```typescript
// 存储密码、Token 等敏感信息
await context.secrets.store('apiToken', 'secret-token-value');
// 读取
const token = await context.secrets.get('apiToken');
// 删除
await context.secrets.delete('apiToken');
// 监听变化
context.secrets.onDidChange(e => {
console.log('Secret changed:', e.key);
});
```
---
## 8. 消息通知
### 8.1 信息提示
```typescript
// 普通信息
vscode.window.showInformationMessage('Operation completed!');
// 警告
vscode.window.showWarningMessage('This action may cause issues');
// 错误
vscode.window.showErrorMessage('Operation failed!');
```
### 8.2 带按钮的提示
```typescript
const result = await vscode.window.showInformationMessage(
'Do you want to continue?',
'Yes',
'No',
'Cancel'
);
if (result === 'Yes') {
// 用户点击了 Yes
}
```
### 8.3 输入框
```typescript
// 简单输入
const input = await vscode.window.showInputBox({
prompt: 'Enter your name',
placeHolder: 'John Doe',
validateInput: (value) => {
return value.length < 3 ? 'Name too short' : null;
}
});
// 快速选择
const selected = await vscode.window.showQuickPick(
['Option 1', 'Option 2', 'Option 3'],
{
placeHolder: 'Select an option',
canPickMany: false
}
);
```
### 8.4 进度提示
```typescript
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: 'Processing...',
cancellable: true
},
async (progress, token) => {
token.onCancellationRequested(() => {
console.log('User canceled');
});
progress.report({ increment: 0, message: 'Starting...' });
await doWork1();
progress.report({ increment: 50, message: 'Half done...' });
await doWork2();
progress.report({ increment: 100, message: 'Complete!' });
}
);
```
---
## 9. 语言特性支持
### 9.1 代码补全
```typescript
const provider = vscode.languages.registerCompletionItemProvider(
'javascript',
{
provideCompletionItems(document, position) {
const item = new vscode.CompletionItem('myFunction');
item.kind = vscode.CompletionItemKind.Function;
item.detail = 'My custom function';
item.documentation = 'This is a custom function';
item.insertText = new vscode.SnippetString('myFunction($1)$0');
return [item];
}
},
'.' // 触发字符
);
context.subscriptions.push(provider);
```
### 9.2 悬停提示
```typescript
const provider = vscode.languages.registerHoverProvider('javascript', {
provideHover(document, position) {
const range = document.getWordRangeAtPosition(position);
const word = document.getText(range);
return new vscode.Hover([
`**${word}**`,
'This is a hover tooltip'
]);
}
});
```
### 9.3 诊断(错误提示)
```typescript
const diagnosticCollection = vscode.languages.createDiagnosticCollection('myExtension');
context.subscriptions.push(diagnosticCollection);
function updateDiagnostics(document: vscode.TextDocument) {
const diagnostics: vscode.Diagnostic[] = [];
const text = document.getText();
const regex = /TODO/g;
let match;
while ((match = regex.exec(text))) {
const range = new vscode.Range(
document.positionAt(match.index),
document.positionAt(match.index + match[0].length)
);
const diagnostic = new vscode.Diagnostic(
range,
'TODO found',
vscode.DiagnosticSeverity.Warning
);
diagnostics.push(diagnostic);
}
diagnosticCollection.set(document.uri, diagnostics);
}
```
---
## 10. 调试和诊断
### 10.1 输出通道
```typescript
const outputChannel = vscode.window.createOutputChannel('My Extension');
context.subscriptions.push(outputChannel);
outputChannel.appendLine('Extension activated');
outputChannel.show(); // 显示输出面板
```
### 10.2 日志记录
```typescript
// 使用 LogOutputChannel带时间戳
const logger = vscode.window.createOutputChannel('My Extension', { log: true });
logger.trace('Trace message');
logger.debug('Debug message');
logger.info('Info message');
logger.warn('Warning message');
logger.error('Error message');
```
### 10.3 错误处理
```typescript
try {
await riskyOperation();
} catch (error) {
if (error instanceof Error) {
vscode.window.showErrorMessage(`Error: ${error.message}`);
logger.error(error.stack || error.message);
}
}
```
---
## 最佳实践总结
### ✅ 推荐做法
1. **资源管理**:所有 disposable 对象都放入 `context.subscriptions`
2. **延迟激活**:使用 `onStartupFinished` 而不是 `*`
3. **异步操作**:使用 `async/await` 处理异步操作
4. **错误处理**:捕获异常并给用户友好提示
5. **类型安全**:充分利用 TypeScript 类型系统
6. **状态持久化**:使用 `globalState`/`workspaceState` 保存状态
7. **敏感信息**:使用 `secrets` API 存储 Token、密码等
### ❌ 避免做法
1. 不要在 `activate` 中执行耗时操作
2. 不要忘记清理资源监听器、Webview 等)
3. 不要在 Webview 中直接访问文件系统
4. 不要在配置中存储敏感信息
5. 不要阻塞主线程(使用 Worker 或异步操作)
---
## 参考资源
- [VS Code Extension API 官方文档](https://code.visualstudio.com/api)
- [Extension Samples](https://github.com/microsoft/vscode-extension-samples)
- [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)

View File

@ -0,0 +1,42 @@
# 代码快速添加到对话功能
## 功能说明
选中代码后,通过右键菜单/小灯泡/快捷键Ctrl+Shift+I将代码作为上下文添加到聊天面板输入框上方。
## 实现方式
### 1. Code Action Provider
`src/providers/codeActionProvider.ts` - 提供小灯泡菜单选项
### 2. 命令注册
`src/extension.ts` - 注册 `ic-coder.addCodeToChat` 命令,发送消息到 webview
### 3. 全局引用
`src/panels/ICHelperPanel.ts` - 保存 panel 到 `(global as any).currentICHelperPanel`
### 4. 上下文显示
`src/views/contextDisplay.ts` - 添加 `code` 类型支持和 `addCodeContext` 消息处理
### 5. 配置
`package.json` - 配置命令、右键菜单、快捷键
## 用户体验
1. 选中代码
2. 右键/小灯泡/Ctrl+Shift+I
3. 代码显示为上下文项:`文件名.v:10-25` 📄
4. 输入问题发送(代码自动作为上下文)
## 数据结构
代码上下文存储为 JSON
```json
{
"fileName": "路径",
"startLine": 10,
"endLine": 25,
"code": "代码内容",
"languageId": "verilog"
}
```

View File

@ -0,0 +1,294 @@
# 删除文件确认功能实现文档
## 1. 功能概述
在 AI 返回删除文件命令时,前端拦截并弹出确认对话框,用户确认后才执行删除操作。
## 2. 架构设计
### 2.1 消息流程
```
AI 后端 → 删除文件工具调用 → 前端拦截 → 用户确认对话框
确定/取消
执行删除/返回取消结果
返回 TOOL_EXECUTION_RESULT
AI 后端
```
### 2.2 关键原则
**前端必须返回结果**:无论用户选择什么,前端都必须向后端返回 `TOOL_EXECUTION_RESULT`,否则后端会等待超时。
## 3. 实现方案
### 3.1 修改位置
文件:`src/utils/messageHandler.ts`
在处理工具调用的函数中,找到删除文件的工具处理逻辑。
### 3.2 核心代码实现
```typescript
/**
* 处理删除文件工具调用(带用户确认)
*/
async function handleDeleteFileTool(
toolCall: any,
panel: vscode.WebviewPanel
): Promise<ToolExecutionResult> {
const filePath = toolCall.arguments.filePath; // 根据实际参数名调整
// 弹出确认对话框
const confirmed = await vscode.window.showWarningMessage(
`确定要删除文件吗?\n\n${filePath}`,
{
modal: true, // 模态对话框,阻止其他操作
detail: '此操作不可撤销'
},
'确定删除',
'取消'
);
// 用户确认删除
if (confirmed === '确定删除') {
try {
// 执行删除操作
const uri = vscode.Uri.file(filePath);
await vscode.workspace.fs.delete(uri, {
recursive: false, // 如果是目录需要设置为 true
useTrash: true // 移到回收站而非永久删除(推荐)
});
// 返回成功结果
return {
type: 'TOOL_EXECUTION_RESULT',
toolCallId: toolCall.id,
result: JSON.stringify({
success: true,
message: `文件已删除: ${filePath}`
})
};
} catch (error) {
// 删除失败
return {
type: 'TOOL_EXECUTION_RESULT',
toolCallId: toolCall.id,
result: JSON.stringify({
success: false,
error: `删除失败: ${error.message}`
})
};
}
}
// 用户取消或关闭对话框
return {
type: 'TOOL_EXECUTION_RESULT',
toolCallId: toolCall.id,
result: JSON.stringify({
success: false,
message: '用户取消了删除操作'
})
};
}
```
### 3.3 集成到消息处理流程
`messageHandler.ts` 的工具调用处理逻辑中:
```typescript
// 示例:在处理工具调用的地方
async function handleToolCall(toolCall: any, panel: vscode.WebviewPanel) {
switch (toolCall.name) {
case 'deleteFile': // 根据实际工具名称调整
return await handleDeleteFileTool(toolCall, panel);
case 'deleteDirectory': // 如果有删除目录的工具
return await handleDeleteDirectoryTool(toolCall, panel);
// ... 其他工具
}
}
```
## 4. 用户体验优化
### 4.1 对话框样式
```typescript
const confirmed = await vscode.window.showWarningMessage(
`确定要删除文件吗?\n\n📄 ${path.basename(filePath)}\n📁 ${path.dirname(filePath)}`,
{
modal: true,
detail: '⚠️ 文件将被移到回收站,可以恢复'
},
'确定删除',
'取消'
);
```
### 4.2 批量删除优化
如果 AI 一次返回多个删除操作:
```typescript
// 方案 1逐个确认
for (const file of filesToDelete) {
await handleDeleteFileTool(file, panel);
}
// 方案 2批量确认推荐
const confirmed = await vscode.window.showWarningMessage(
`确定要删除以下 ${filesToDelete.length} 个文件吗?\n\n${filesToDelete.join('\n')}`,
{ modal: true },
'全部删除',
'取消'
);
```
## 5. 安全考虑
### 5.1 使用回收站
```typescript
await vscode.workspace.fs.delete(uri, {
useTrash: true // 移到回收站,可恢复
});
```
### 5.2 路径验证
```typescript
// 防止删除工作区外的文件
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders) {
return { success: false, error: '未打开工作区' };
}
const isInWorkspace = workspaceFolders.some(folder =>
filePath.startsWith(folder.uri.fsPath)
);
if (!isInWorkspace) {
return { success: false, error: '只能删除工作区内的文件' };
}
```
### 5.3 敏感文件保护
```typescript
const protectedFiles = [
'package.json',
'tsconfig.json',
'.git',
'node_modules'
];
const fileName = path.basename(filePath);
if (protectedFiles.includes(fileName)) {
vscode.window.showErrorMessage(`不允许删除系统文件: ${fileName}`);
return { success: false, error: '受保护的文件' };
}
```
## 6. 错误处理
### 6.1 常见错误
```typescript
try {
await vscode.workspace.fs.delete(uri, { useTrash: true });
} catch (error) {
if (error.code === 'FileNotFound') {
return { success: false, error: '文件不存在' };
}
if (error.code === 'NoPermissions') {
return { success: false, error: '没有删除权限' };
}
return { success: false, error: error.message };
}
```
## 7. 测试场景
### 7.1 基本测试
- [ ] 用户点击"确定删除" → 文件被删除
- [ ] 用户点击"取消" → 文件保留,返回取消消息
- [ ] 用户关闭对话框 → 文件保留,返回取消消息
- [ ] 文件不存在 → 返回错误消息
- [ ] 没有删除权限 → 返回错误消息
### 7.2 边界测试
- [ ] 删除工作区外的文件 → 拒绝
- [ ] 删除受保护文件 → 拒绝
- [ ] 批量删除 → 正确处理
- [ ] 后端收到取消消息后继续对话 → 流程正常
## 8. 配置选项(可选)
可以添加用户设置来控制行为:
```json
// package.json
"configuration": {
"properties": {
"ic-coder.confirmDelete": {
"type": "boolean",
"default": true,
"description": "删除文件前是否需要确认"
},
"ic-coder.useTrash": {
"type": "boolean",
"default": true,
"description": "删除文件时移到回收站而非永久删除"
}
}
}
```
读取配置:
```typescript
const config = vscode.workspace.getConfiguration('ic-coder');
const needConfirm = config.get<boolean>('confirmDelete', true);
const useTrash = config.get<boolean>('useTrash', true);
if (needConfirm) {
// 弹出确认对话框
}
```
## 9. 总结
### 9.1 后端是否需要修改?
**不需要**。后端继续返回删除工具调用,前端负责:
1. 拦截工具调用
2. 弹出确认对话框
3. 执行或取消删除
4. **必须返回结果给后端**
### 9.2 关键要点
- ✅ 前端必须返回 `TOOL_EXECUTION_RESULT`
- ✅ 使用 `useTrash: true` 提高安全性
- ✅ 验证文件路径在工作区内
- ✅ 保护敏感文件
- ✅ 提供清晰的错误消息
### 9.3 下一步
1.`messageHandler.ts` 中找到工具调用处理逻辑
2. 实现 `handleDeleteFileTool` 函数
3. 集成到现有流程
4. 测试各种场景
5. 考虑添加用户配置选项

View File

@ -0,0 +1,161 @@
# 个人规则功能需求文档(方案 A本地 `.md` 注入)
## 1. 文档目标
在不改动现有核心对话模式的前提下实现“个人规则Personal Rules”能力
用户可在插件内维护个人规则文本,插件保存到本地 `.md` 文件;每次发起对话时自动读取并随请求传递给后端,由后端注入模型上下文,影响回答风格与行为。
## 2. 范围定义
### 2.1 本期范围MVP
1. 支持用户编辑、保存、启用/停用个人规则。
2. 本地落盘为 `.md` 文件。
3. 发消息时自动加载规则并传给后端。
4. 后端接收结构化字段并注入提示词。
5. 基础异常处理和可观测提示。
### 2.2 非本期范围
1. 云端同步、多设备同步。
2. 规则版本历史/回滚。
3. 多规则集合管理(仅单份个人规则文本)。
4. 团队共享规则。
## 3. 术语与核心概念
1. `Personal Rules`:用户个人偏好与约束文本。
2. `Rules File`本地规则文件Markdown 格式。
3. `Rules Enabled`:规则开关;关闭时不注入。
4. `Rules Injection`:请求时将规则传后端并参与模型上下文构建。
## 4. 用户故事
1. 作为用户,我希望在插件里写下“回答风格、代码习惯、语言偏好”等个人规则。
2. 作为用户,我希望规则保存在本地可见文件中。
3. 作为用户,我希望发消息时自动生效,无需每次重复输入。
4. 作为用户,我希望可以一键关闭规则,临时不生效。
## 5. 功能需求(前端/Webview + 扩展端)
### 5.1 规则管理界面
1. 提供“个人规则”入口。
2. 提供多行编辑框(显示当前规则内容)。
3. 提供“保存”按钮。
4. 提供“启用/停用”开关。
5. 显示当前状态:
6. 规则是否启用。
7. 规则字数/长度。
8. 最近保存时间(可选)。
### 5.2 本地文件存储
1. 规则内容保存到本地 `.md`
2. 推荐文件名:`personal-rules.md`
3. 推荐路径(优先):插件全局存储目录下固定子路径。
4. 文件不存在时可自动创建。
5. 用户可通过“打开规则文件”查看(可选)。
### 5.3 对话发送前处理
1. 用户点击发送消息。
2. 扩展端检查规则开关:
3. 关闭:不读取规则,不传后端。
4. 开启:读取 `.md` 内容。
5. 读取成功且非空时,将规则文本附加到对话请求结构化字段。
6. 读取失败时:提示告警,但不阻断正常对话。
### 5.4 限制与防护
1. 规则长度上限(例如 4000 字符,可配置)。
2. 超限时保存被拒绝,提示用户缩短。
3. 空白内容视为“无规则”。
4. 不允许二进制或非文本写入。
## 6. 功能需求(后端)
### 6.1 请求协议扩展
在现有对话请求结构中增加字段:
1. `personalRules`:字符串,可选。
2. `rulesEnabled`:布尔,可选(便于追踪)。
3. `rulesMeta`:可选元信息(长度、来源)。
### 6.2 注入策略
1. 后端收到 `personalRules` 后,将其注入系统提示层(而非用户消息层)。
2. 注入顺序建议:
3. 系统安全与平台策略。
4. 产品默认系统提示。
5. 用户个人规则。
6. 用户输入。
7.`personalRules` 为空或开关关闭,则跳过注入。
### 6.3 风险控制
1. 规则文本不允许覆盖平台安全策略。
2. 记录本次是否注入规则(日志字段即可)。
3. 异常不应导致整次对话失败(可降级为无规则对话)。
## 7. 前后端对接设计
### 7.1 消息链路
1. Webview 触发 `sendMessage`
2. 扩展端 `messageHandler` 统一处理发送。
3. `messageHandler` 在调用 `dialogService.sendMessage` 前读取个人规则。
4. `dialogService` 组装 `DialogRequest`,带上 `personalRules`
5. `sseHandler` 发起流式请求。
6. 后端注入规则后进入模型推理。
7. 正常走现有 SSE 回传流程。
### 7.2 职责边界
1. Webview展示与编辑不直接拼接最终请求。
2. 扩展端:规则文件读写、开关状态管理、请求组装。
3. 后端:规则注入、优先级控制、审计日志。
## 8. 数据与状态设计
### 8.1 本地文件
1. 文件格式Markdown 纯文本。
2. 内容约定:无强制模板,允许自由文本。
3. 编码UTF-8。
### 8.2 本地配置状态
1. `personalRulesEnabled`:是否启用。
2. `personalRulesPath`:规则文件路径(可固定也可配置)。
3. `lastSavedAt`:最近保存时间(可选)。
## 9. 异常与降级
1. 文件不存在:自动创建空文件,视为无规则。
2. 文件读取失败:弹出提示,继续无规则发送。
3. 文件写入失败:保存失败提示,不更新状态。
4. 后端字段不识别:请求兼容,后端忽略新字段。
5. 后端注入失败:降级为普通对话,记录日志。
## 10. 安全与合规要求
1. 个人规则属于用户本地数据,不主动上传除非发起对话。
2. 日志中避免完整打印规则正文(最多打印长度和哈希)。
3. 后端注入时必须确保平台安全策略优先级更高。
## 11. 验收标准UAT
1. 用户保存规则后,本地存在 `personal-rules.md` 且内容一致。
2. 开启规则发送消息时,请求中可观测到 `personalRules` 字段。
3. 关闭规则发送消息时,请求中不含该字段或为空。
4. 规则文件损坏/读取失败时,不影响正常聊天。
5. 超过长度上限时,前端保存被拒绝且提示明确。
6. 后端日志可确认“本次是否注入个人规则”。
## 12. 迭代建议(下一阶段)
1. 规则模板(代码风格、语言风格、测试偏好)。
2. 项目规则与个人规则合并策略。
3. 云端同步(按 `userId`),多端一致。

View File

@ -0,0 +1,55 @@
# Webpack 打包优化说明
## 优化内容
### 1. 自动模式切换
- 开发模式:保持源码可读性
- 生产模式:自动压缩代码
### 2. 性能优化
- **Tree Shaking**:移除未使用的代码
- **transpileOnly**:跳过类型检查,加快编译速度
- **自动清理**:每次打包自动删除旧文件
### 3. 体积监控
- 单文件超过 2MB 会发出警告
- 帮助及时发现打包体积问题
## 使用方法
### 开发模式
```bash
# 编译(不压缩)
pnpm run compile
# 监听模式(自动重新编译)
pnpm run watch
```
### 生产模式
```bash
# Windows
set NODE_ENV=production && pnpm run package
# macOS/Linux
NODE_ENV=production pnpm run package
```
## 打包结果
- **输出目录**`dist/`
- **入口文件**`dist/extension.js`
- **静态资源**`dist/assets/`
## 性能对比
| 模式 | 体积 | 编译速度 | Source Map |
|------|------|----------|------------|
| 开发 | 较大 | 快 | 完整 |
| 生产 | 小 | 较慢 | 隐藏 |
## 注意事项
1. 开发时使用 `pnpm run watch`,修改代码自动重新编译
2. 发布前必须使用生产模式打包
3. 如果打包体积超过 2MB检查是否引入了不必要的依赖

251
media/USER_MANUAL.md Normal file
View File

@ -0,0 +1,251 @@
# IC Coder 插件端用户手册
欢迎**宁德时代新能源科技股份有限公司**的各位专家使用 IC Coder 企业版!
本手册旨在为贵公司提供插件的安装、配置及使用指导,帮助您快速上手并充分发挥工具的功能。
在使用过程中,如遇任何功能异常、性能问题或操作疑问,欢迎随时联系我们,我们将在第一时间为您提供支持。
若贵司有特定的业务场景或个性化功能需求,也可直接与我们沟通,我们会为贵司进行定制化开发。
| 功能 | 说明 |
| ----------------------- | ---------------------------------------------------- |
| 自研微调模型 | IC Coder 自研顶尖 Max 微调模型,专为 FPGA 开发 |
| 高质量 Verilog 代码生成 | 根据自然语言描述生成符合规范的 Verilog 代码 |
| 自动语法检查 | 自动检测代码语法错误并自动修复 |
| 自动仿真 | 内置编译器,自动编译和仿真 |
| 波形检查工具 | 内置 VCD 波形查看器,自动波形解析 |
| 自主检查迭代 | AI 自动检查代码问题并迭代优化 |
| Diff 功能 | 快速比较代码差异,追踪 AI 修改,可一键确认和撤销修改 |
| Code Action 快捷操作 | Ctrl+L 快捷键或右键菜单快速添加代码到对话 |
| 支持上下文连续对话 | 多轮对话AI 记住之前的交互内容 |
| 会话历史管理 | 自动保存对话记录,随时恢复历史会话 |
## IC Coder 快速入门指南
### 系统要求
- **Visual Studio Code**: 版本 >= 1.60.0
各位专家也可以通过这个链接下载VSCode [下载链接](https://code.visualstudio.com/)
---
### 安装步骤
#### 步骤 1通过 VSIX 文件安装(推荐)
1. **获取安装包**
- 确保您已经获得 `iccoder-Trial-1.0.vsix` 文件
2. **打开 VS Code**
- 启动 Visual Studio Code
3. **安装插件**
有以下三种安装方式:
**方式 A通过命令面板**
-`Ctrl+Shift+P` (Windows/Linux) 或 `Cmd+Shift+P` (macOS) 打开命令面板
- 输入 `Extensions: Install from VSIX...`
- 选择 `iccoder-Trial-1.0.vsix` 文件
- 等待安装完成
![安装方式1.png](./manual/安装方式1.png)
**方式 B通过扩展视图**
- 点击左侧活动栏的扩展图标(或按 `Ctrl+Shift+X`
- 点击扩展视图右上角的 `...` (更多操作)
- 选择 `从 VSIX 安装...`
- 选择 `iccoder-Trial-1.0.vsix` 文件
![安装方式2.png](./manual/安装方式2.png)
**方式 C通过命令行**
```bash
code --install-extension iccoder-Trial-1.0.vsix
```
4. **重启 VS Code**
- 安装完成后,建议重启 VS Code 以确保插件正常加载
#### 步骤 2打开 IC Coder 界面
**登录后会自动打开**,手动打开也有以下几种方式:
**方式 1通过侧边栏**
- 点击左侧活动栏的 IC Coder 图标
- 侧边栏会显示 IC Coder 聊天界面
![侧边栏打开.png](./manual/侧边栏打开.png)
**方式 2通过命令面板**
- 按 `Ctrl+Shift+P` (Windows/Linux) 或 `Cmd+Shift+P` (macOS)
- 输入以下命令之一:
- `IC Coder: 打开聊天` - 打开聊天界面
- `打开 IC Coder 助手` - 打开助手面板
![命令面板打开.png](./manual/命令面板打开.png)
---
#### 步骤 3开始使用
插件已预配置好后端服务,安装后即可直接使用,无需手动配置。
![聊天界面.png](./manual/聊天界面.png)
### 故障排除
#### 问题 :插件无法安装
**症状**:安装 VSIX 文件时报错
#### 解决方案:
- 确认 VS Code 版本 >= 1.60.0
- 检查 VSIX 文件是否完整(未损坏)
- 尝试以管理员权限运行 VS Code
- 清除 VS Code 缓存后重试
### 完整使用流程示例
下面通过一个完整的案例,展示如何使用 IC Coder 从需求到代码生成的全过程。
#### 步骤 1输入设计需求
在对话框中输入设计需求,例如:
```
我需要设计一个 8 位加法器,要求有进位输入和进位输出
```
点击**发送**按钮。
![输入需求.png](./manual/输入需求.png)
#### 步骤 2AI 询问补充信息
AI 会根据需求,询问一些关键的设计细节。例如:
- 是否需要溢出检测?
- 时钟频率要求是多少?
- 是否需要流水线设计?
您只需要根据实际需求**选择相应的选项或者直接输入需求**即可AI 会根据您的选择生成最合适的设计方案。
#### 步骤 3确认 AI 生成的任务列表
AI 会根据您的需求和补充信息生成一个详细的任务列表Todo List
![确认任务.png](./manual/确认任务.png)
仔细查看任务列表,确认无误后点击**确认**按钮AI 将开始执行。
#### 步骤 4观察 AI 执行过程
AI 开始工作后,您可以在对话框中实时看到所有执行步骤:
![观察执行过程.png](./manual/观察执行过程.png)
每个步骤完成后,任务列表中对应的项目会被标记为完成状态。
#### 步骤 5仿真运行与结果查看
当 AI 完成代码生成后,会自动运行仿真验证:
![仿真运行结果.png](./manual/仿真运行结果.png)
#### 步骤 6查看生成的文件
所有生成的文件会自动保存到您的工作目录中:
```
project/
├── src/
│ └── tb_adder_8bit.v # RTL 设计文件
├── sim/
│ └── tb_adder_8bit # 测试平台文件
└── tb_adder_8bit.vcd # 波形文件
```
您可以在 VS Code 的文件资源管理器中直接打开这些文件进行查看或修改。
#### 步骤8继续对话
如果您对给出的结果不太满意您可以告诉IC Coder您想具体修改的地方或者文件
#### 使用流程总结
整个使用流程可以概括为:
- **输入需求** → 在对话框中描述您的设计需求
- **回答问题** → 根据 AI 的询问选择合适的选项
- **确认任务** → 查看并确认 AI 生成的任务列表
- **观察执行** → 实时查看 AI 的所有执行步骤
- **查看结果** → 仿真成功后查看生成的文件
#### 使用提示
**如何描述需求更准确?**
- **明确功能**:清楚说明模块要实现什么功能
- **指定参数**:说明位宽、时钟频率等关键参数
- **特殊要求**:如果有特殊的时序要求或接口规范,请明确说明
**示例:**
```
好的描述:设计一个 16 位的 FIFO深度为 256支持异步读写
不够清晰:帮我写一个 FIFO
```
#### 常见问题
**Q: 仿真失败了怎么办?**
A: AI 会根据错误自动修复代码并重新仿真。
**Q: 可以修改生成的代码吗?**
A: 可以,可以直接编辑文件,然后告诉 AI 重新运行仿真。
**Q: 可以导入已有的代码吗?**
A: 可以,在工作区中打开对应的代码文件夹,然后直接在对话中告诉 AI 您要修改或优化哪个文件AI 会读取并进行修改。
**Q: 如何查看 AI 的思考过程?**
A: 在执行过程中AI 会实时显示每一步的操作和决策依据。
**Q: 如何保存对话历史?**
A: 对话历史会自动保存在本地,可以点击历史对话查看历史会话记录。
---
### 卸载插件
如需卸载插件:
1. 打开扩展视图
2. 找到 "IC Coder" 插件
3. 点击卸载按钮
4. 重启 VS Code
---
### 注意事项
1. **需提前打开一个文件夹作为工作区**,否则会准确的为您服务
![打开文件夹.png](./manual/打开文件夹.png)
2. **开箱即用**
- 插件已预配置后端服务,无需手动设置
- 安装后即可直接使用所有功能
---
**祝您使用愉快!如有问题欢迎反馈。**

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -2,7 +2,7 @@
"name": "iccoder",
"displayName": "IC Coder: Agentic Verilog Platform",
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
"version": "1.0.5",
"version": "1.0.12",
"publisher": "ICCoderAgenticVerilogPlatform",
"engines": {
"vscode": "^1.80.0"
@ -54,6 +54,28 @@
"command": "ic-coder.testNotification",
"title": "测试系统通知",
"category": "IC Coder"
},
{
"command": "ic-coder.addCodeToChat",
"title": "添加到 IC Coder 对话",
"category": "IC Coder"
}
],
"menus": {
"editor/context": [
{
"command": "ic-coder.addCodeToChat",
"when": "editorHasSelection",
"group": "9_cutcopypaste"
}
]
},
"keybindings": [
{
"command": "ic-coder.addCodeToChat",
"key": "ctrl+l",
"mac": "cmd+l",
"when": "editorTextFocus && editorHasSelection"
}
],
"viewsContainers": {
@ -135,6 +157,7 @@
"@vscode/test-cli": "^0.0.12",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.7.1",
"copy-webpack-plugin": "^14.0.0",
"eslint": "^9.39.1",
"ts-loader": "^9.5.4",
"typescript": "^5.9.3",
@ -142,14 +165,6 @@
"webpack": "^5.103.0",
"webpack-cli": "^6.0.1"
},
"files": [
"dist",
"media",
"tools",
"src/assets",
"LICENSE",
"CHANGELOG.md"
],
"dependencies": {
"@wavedrom/doppler": "^1.14.0",
"eventsource-parser": "^3.0.6",

24
pnpm-lock.yaml generated
View File

@ -57,6 +57,9 @@ importers:
'@vscode/vsce':
specifier: ^3.7.1
version: 3.7.1
copy-webpack-plugin:
specifier: ^14.0.0
version: 14.0.0(webpack@5.103.0)
eslint:
specifier: ^9.39.1
version: 9.39.1
@ -823,6 +826,12 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
copy-webpack-plugin@14.0.0:
resolution: {integrity: sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==}
engines: {node: '>= 20.9.0'}
peerDependencies:
webpack: ^5.1.0
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@ -1928,6 +1937,10 @@ packages:
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
serialize-javascript@7.0.4:
resolution: {integrity: sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==}
engines: {node: '>=20.0.0'}
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
@ -3297,6 +3310,15 @@ snapshots:
convert-source-map@2.0.0: {}
copy-webpack-plugin@14.0.0(webpack@5.103.0):
dependencies:
glob-parent: 6.0.2
normalize-path: 3.0.0
schema-utils: 4.3.3
serialize-javascript: 7.0.4
tinyglobby: 0.2.15
webpack: 5.103.0(webpack-cli@6.0.1)
core-util-is@1.0.3: {}
cross-spawn@7.0.6:
@ -4431,6 +4453,8 @@ snapshots:
dependencies:
randombytes: 2.1.0
serialize-javascript@7.0.4: {}
setimmediate@1.0.5: {}
shallow-clone@3.0.1:

View File

@ -29,6 +29,9 @@ export interface IccoderConfig {
serviceTier: ServiceTier;
}
/** 自定义配置缓存 */
let customConfig: Partial<IccoderConfig> | null = null;
/** 环境配置 */
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
/** 本地开发环境 - 通过 Gateway 路由 */
@ -38,7 +41,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
loginUrl: "http://localhost/login",
timeout: 300000,
userId: "default-user",
serviceTier: "max", // 默认使用 max
serviceTier: "max",
},
/** 测试服务器环境 - 通过 Gateway 路由 */
test: {
@ -60,6 +63,13 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
},
};
/**
* 设置自定义配置
*/
export function setCustomConfig(config: Partial<IccoderConfig>) {
customConfig = config;
}
/**
* 获取当前环境
*/
@ -71,7 +81,14 @@ export function getCurrentEnv(): Environment {
* 获取配置项
*/
export function getConfig(): IccoderConfig {
return { ...ENV_CONFIG[CURRENT_ENV] };
const baseConfig = { ...ENV_CONFIG[CURRENT_ENV] };
// 合并自定义配置(空字符串表示使用默认)
if (customConfig?.backendUrl && customConfig.backendUrl !== '') {
baseConfig.backendUrl = customConfig.backendUrl;
}
return baseConfig;
}
/**

View File

@ -2,49 +2,82 @@ import * as vscode from "vscode";
import { ICViewProvider } from "./views/ICViewProvider";
import { showICHelperPanel } from "./panels/ICHelperPanel";
import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel";
import { UserManualPanel } from "./panels/UserManualPanel";
import { ChatHistoryManager } from "./utils/chatHistoryManager";
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
import { VCDFileServer } from "./services/vcdFileServer";
import { initUserService } from "./services/userService";
import { initCreditsService } from "./services/creditsService";
import { isTokenExpired } from "./utils/jwtUtils";
import { NotificationService } from "./services/notificationService";
import { InvitationService } from "./services/invitationService";
import { ICCoderCodeActionProvider } from "./providers/codeActionProvider";
import { setCustomConfig } from "./config/settings";
export async function activate(context: vscode.ExtensionContext) {
console.log("🎉 IC Coder 插件已激活!");
// 加载保存的配置
const savedSettings = context.globalState.get('generalSettings') as any;
if (savedSettings?.backendUrl) {
setCustomConfig({
backendUrl: savedSettings.backendUrl,
});
}
// 创建装饰类型(代码旁边的提示)
const decorationType = vscode.window.createTextEditorDecorationType({
after: {
contentText: ' Ctrl+L 添加到 IC Coder 对话',
color: '#888',
fontStyle: 'italic',
margin: '0 0 0 1em'
}
});
// 更新装饰
const updateDecorations = () => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
if (!editor.selection.isEmpty) {
const range = new vscode.Range(editor.selection.end, editor.selection.end);
const decoration = { range };
editor.setDecorations(decorationType, [decoration]);
} else {
editor.setDecorations(decorationType, []);
}
};
context.subscriptions.push(
vscode.window.onDidChangeTextEditorSelection(updateDecorations),
vscode.window.onDidChangeActiveTextEditor(updateDecorations)
);
updateDecorations();
// 初始化通知服务
const notificationService = NotificationService.getInstance(context);
console.log('[Extension] 通知服务已初始化');
// 【关键】在创建 AuthProvider 之前,先检查并清除过期的 session
const storedSessions = context.globalState.get<any[]>('icCoderSessions', []);
console.log('[Extension] 检查 sessions 数量:', storedSessions.length);
if (storedSessions.length > 0) {
const session = storedSessions[0];
const token = session.accessToken;
console.log('[Extension] 检查 token 是否过期...');
if (token) {
const expired = isTokenExpired(token);
console.log('[Extension] token 过期检查结果:', expired);
if (expired) {
// 必须等待清除完成后再创建 AuthProvider
await context.globalState.update('icCoderSessions', []);
await context.globalState.update('icCoderUserInfo', undefined);
console.log('[Extension] Token 已过期,已清除所有登录状态');
}
}
}
// 初始化用户服务
initUserService(context);
// 初始化 Credits 服务
initCreditsService(context);
// 【已禁用】登录和 token 验证 - 无需登录即可使用
// const storedSessions = context.globalState.get<any[]>('icCoderSessions', []);
// console.log('[Extension] 检查 sessions 数量:', storedSessions.length);
//
// if (storedSessions.length > 0) {
// const session = storedSessions[0];
// const token = session.accessToken;
// console.log('[Extension] 检查 token 是否过期...');
//
// if (token) {
// const expired = isTokenExpired(token);
// console.log('[Extension] token 过期检查结果:', expired);
//
// if (expired) {
// await context.globalState.update('icCoderSessions', []);
// await context.globalState.update('icCoderUserInfo', undefined);
// console.log('[Extension] Token 已过期,已清除所有登录状态');
// }
// }
// }
// 初始化 VCD 文件服务器
const vcdFileServer = new VCDFileServer(context.extensionUri);
@ -59,25 +92,18 @@ export async function activate(context: vscode.ExtensionContext) {
dispose: () => vcdFileServer.stop()
});
// 注册 Authentication Provider(此时 icCoderSessions 已经被清除)
// 【已禁用】Authentication Provider 注册 - 无需登录
const authProvider = new ICCoderAuthenticationProvider(context);
context.subscriptions.push(
vscode.authentication.registerAuthenticationProvider(
"iccoder",
"IC Coder",
authProvider
)
);
// 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");
}
}, () => {
// 未登录,不做任何操作
});
// 【已禁用】登录状态检查 - 直接打开聊天面板
vscode.commands.executeCommand("ic-coder.openChat");
// 注册命令:打开助手面板
const openPanelCommand = vscode.commands.registerCommand(
@ -156,23 +182,43 @@ export async function activate(context: vscode.ExtensionContext) {
}
);
// 注册命令:打开用户手册
const openUserManualCommand = vscode.commands.registerCommand(
"ic-coder.openUserManual",
() => {
UserManualPanel.render(context.extensionUri);
}
);
// 注册命令:用户登录
const loginCommand = vscode.commands.registerCommand(
"ic-coder.login",
async () => {
async (options?: { forceReauth?: boolean }) => {
try {
// 先清除 session 偏好,避免 VSCode 弹出"账户不一致"确认框
try {
await vscode.authentication.getSession("iccoder", [], {
clearSessionPreference: true,
createIfNone: false
});
} catch {
// 忽略错误
const forceReauth = options?.forceReauth === true;
const session = await vscode.authentication.getSession("iccoder", [], {
createIfNone: false,
});
const expired = session?.accessToken
? isTokenExpired(session.accessToken)
: null;
// 会话仍有效时,直接打开聊天面板
if (session && expired === false && !forceReauth) {
vscode.commands.executeCommand("ic-coder.openChat");
return;
}
// 创建新 session
await vscode.authentication.getSession("iccoder", [], { createIfNone: true });
// 1) 清空当前登录状态信息
await authProvider.clearSessionsForRelogin();
await context.globalState.update("icCoderSessions", []);
await context.globalState.update("icCoderUserInfo", undefined);
// 2) 重新登录(强制新会话)
await vscode.authentication.getSession("iccoder", [], {
clearSessionPreference: true,
forceNewSession: true,
});
} catch (error) {
vscode.window.showErrorMessage(`登录失败: ${error}`);
}
@ -238,6 +284,81 @@ export async function activate(context: vscode.ExtensionContext) {
}
);
// 注册命令:将选中代码添加到对话
const addCodeToChat = vscode.commands.registerCommand(
"ic-coder.addCodeToChat",
async () => {
console.log('[addCodeToChat] 命令触发');
const editor = vscode.window.activeTextEditor;
if (!editor) {
console.log('[addCodeToChat] 没有活动编辑器');
return;
}
const selection = editor.selection;
const selectedText = editor.document.getText(selection);
if (!selectedText) {
vscode.window.showWarningMessage("请先选择代码");
return;
}
const fileName = editor.document.fileName;
const startLine = selection.start.line + 1;
const endLine = selection.end.line + 1;
// 检查是否已有打开的面板
let panel = (global as any).currentICHelperPanel;
let needCreatePanel = false;
if (!panel) {
needCreatePanel = true;
} else {
// 尝试访问 webview如果抛出异常说明已销毁
try {
const _ = panel.webview;
} catch (e) {
needCreatePanel = true;
}
}
console.log('[addCodeToChat] 需要创建面板:', needCreatePanel);
if (needCreatePanel) {
console.log('[addCodeToChat] 正在打开面板...');
await showICHelperPanel(context);
panel = (global as any).currentICHelperPanel;
console.log('[addCodeToChat] 面板打开后状态:', panel ? '成功' : '失败');
// 如果面板仍未创建(如未登录),直接返回
if (!panel) {
console.log('[addCodeToChat] 面板创建失败,退出');
return;
}
}
// 发送代码上下文
console.log('[addCodeToChat] 准备发送代码到面板');
setTimeout(() => {
try {
if (panel?.webview) {
console.log('[addCodeToChat] 发送 addCodeContext 消息');
panel.webview.postMessage({
command: 'addCodeContext',
fileName,
startLine,
endLine,
code: selectedText,
languageId: editor.document.languageId
});
}
} catch (e) {
console.log('[addCodeToChat] 发送消息失败:', e);
}
}, 500);
}
);
// 注册命令:查看会话历史
// TODO: 这些命令需要根据新的任务架构重新实现
// 暂时注释掉,等待重新实现
@ -300,16 +421,25 @@ export async function activate(context: vscode.ExtensionContext) {
// 注册 VCD 自定义编辑器
const vcdEditorProvider = VCDViewerEditorProvider.register(context, vcdFileServer);
// 注册 Code Action Provider
const codeActionProvider = vscode.languages.registerCodeActionsProvider(
{ scheme: 'file' },
new ICCoderCodeActionProvider(),
{ providedCodeActionKinds: [vscode.CodeActionKind.RefactorRewrite] }
);
// 添加到订阅
context.subscriptions.push(
openPanelCommand,
openChatCommand,
openVCDViewerCommand,
openVCDViewerInBrowserCommand,
openUserManualCommand,
loginCommand,
logoutCommand,
changeInvitationCodeCommand,
testNotificationCommand,
addCodeToChat,
// testTrialUserCommand,
// testExpiredUserCommand,
// TODO: 等待重新实现这些命令
@ -320,7 +450,8 @@ export async function activate(context: vscode.ExtensionContext) {
// clearHistoryCommand,
// searchSessionCommand,
viewRegistration,
vcdEditorProvider
vcdEditorProvider,
codeActionProvider
);
}

View File

@ -18,50 +18,12 @@ import {
startChangeSession,
handleOpenFileDiff,
} from "../utils/messageHandler";
import { setCustomConfig } from "../config/settings";
import { compactDialog } from "../services/apiClient";
import { VCDViewerPanel } from "./VCDViewerPanel";
import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { MessageType } from "../types/chatHistory";
import { getCachedUserInfo } from "../services/userService";
import { isTokenExpired } from "../utils/jwtUtils";
import { setBalanceUpdateCallback } from "../services/creditsService";
/**
* 获取会员等级图标 URI
*/
function getTierIconUri(
webview: vscode.Webview,
context: vscode.ExtensionContext,
tierCode?: string,
): string | undefined {
if (!tierCode) {
return undefined;
}
const tierIconMap: Record<string, string> = {
BASIC: "free.png",
TRIAL: "PRO-Try.png",
ADVANCED: "PRO.png",
PROFESSIONAL: "PRO+.png",
};
const iconFile = tierIconMap[tierCode];
if (!iconFile) {
return undefined;
}
const iconUri = webview.asWebviewUri(
vscode.Uri.joinPath(
context.extensionUri,
"src",
"assets",
"titleIcon",
iconFile,
),
);
return iconUri.toString();
}
/**
* 创建并显示 IC 助手面板
@ -70,57 +32,35 @@ export async function showICHelperPanel(
context: vscode.ExtensionContext,
viewColumn?: vscode.ViewColumn,
) {
// 检查 token 是否过期
let token: string | undefined;
try {
const session = await vscode.authentication.getSession("iccoder", [], {
createIfNone: false,
});
token = session?.accessToken;
} catch (error) {
console.warn("[ICHelperPanel] 获取 session 失败:", error);
}
if (token && isTokenExpired(token)) {
// 清除过期的 session
await context.globalState.update("icCoderSessions", []);
await context.globalState.update("icCoderUserInfo", undefined);
const action = await vscode.window.showWarningMessage(
"登录已过期,请重新登录",
"立即登录",
);
if (action === "立即登录") {
vscode.commands.executeCommand("ic-coder.login");
}
return;
}
// 检查用户是否已登录
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面板
// 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", {
// forceReauth: true,
// });
// }
// });
// return;
// }
// } catch (error) {
// vscode.window
// .showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
// .then((selection) => {
// if (selection === "立即登录") {
// vscode.commands.executeCommand("ic-coder.login", {
// forceReauth: true,
// });
// }
// });
// return;
// }
// 创建WebView面板
const panel = vscode.window.createWebviewPanel(
@ -132,11 +72,14 @@ export async function showICHelperPanel(
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.joinPath(context.extensionUri, "media"),
vscode.Uri.joinPath(context.extensionUri, "src", "assets"),
vscode.Uri.joinPath(context.extensionUri, "dist", "assets"),
],
},
);
// 保存 panel 引用到全局
(global as any).currentICHelperPanel = panel;
// 为面板生成唯一ID
const panelId = `panel_${Date.now()}_${Math.random()
.toString(36)
@ -160,7 +103,7 @@ export async function showICHelperPanel(
const autoIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(
context.extensionUri,
"src",
"dist",
"assets",
"model",
"Auto.png",
@ -169,7 +112,7 @@ export async function showICHelperPanel(
const liteIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(
context.extensionUri,
"src",
"dist",
"assets",
"model",
"lite.png",
@ -178,7 +121,7 @@ export async function showICHelperPanel(
const syIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(
context.extensionUri,
"src",
"dist",
"assets",
"model",
"Sy.png",
@ -187,7 +130,7 @@ export async function showICHelperPanel(
const maxIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(
context.extensionUri,
"src",
"dist",
"assets",
"model",
"Max.png",
@ -198,7 +141,7 @@ export async function showICHelperPanel(
const qrCodeUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(
context.extensionUri,
"src",
"dist",
"assets",
"QRCode",
"wx.png",
@ -221,81 +164,6 @@ export async function showICHelperPanel(
logoUri.toString(),
);
// 获取并发送用户信息到 webview
try {
// 优先使用缓存的用户信息
let userInfo = getCachedUserInfo();
if (userInfo) {
// 使用缓存的用户信息
console.log("[ICHelperPanel] 使用缓存的用户信息:", userInfo);
console.log("[ICHelperPanel] Credits 余额:", userInfo.credits);
const tierIconUrl = getTierIconUri(
panel.webview,
context,
userInfo.membership?.tierCode,
);
const messageData = {
command: "updateUserInfo",
userInfo: {
userId: userInfo.userId,
nickname: userInfo.nickname,
username: userInfo.username,
credits: userInfo.credits,
membership: userInfo.membership,
},
tierIconUrl: tierIconUrl,
};
console.log("[ICHelperPanel] 发送用户信息到前端:", messageData);
panel.webview.postMessage(messageData);
} else {
// 如果没有缓存,从 session 中获取
const session = await vscode.authentication.getSession("iccoder", [], {
createIfNone: false,
});
if (session) {
console.log(
"[ICHelperPanel] 从 session 获取用户信息, account:",
session.account,
);
panel.webview.postMessage({
command: "updateUserInfo",
userInfo: {
userId: session.account.id,
nickname: session.account.label,
username: session.account.label,
},
});
}
}
} catch (error) {
console.error("[ICHelperPanel] 获取用户信息失败:", error);
}
// 设置余额更新回调
setBalanceUpdateCallback((balance: number) => {
const userInfo = getCachedUserInfo();
if (userInfo) {
userInfo.credits = balance;
const tierIconUrl = getTierIconUri(
panel.webview,
context,
userInfo.membership?.tierCode,
);
panel.webview.postMessage({
command: "updateUserInfo",
userInfo: {
userId: userInfo.userId,
nickname: userInfo.nickname,
username: userInfo.username,
credits: balance,
membership: userInfo.membership,
},
tierIconUrl: tierIconUrl,
});
}
});
// 检查是否有待发送的消息
const pendingMessage = context.globalState.get("pendingMessage") as any;
if (pendingMessage) {
@ -430,6 +298,7 @@ export async function showICHelperPanel(
message.askId,
message.selected,
message.customInput,
message.answers
);
break;
// 新增:中止对话
@ -484,6 +353,78 @@ export async function showICHelperPanel(
// 退出登录
vscode.commands.executeCommand("ic-coder.logout");
break;
case "openFile":
// 打开文件
if (message.filePath) {
const path = require('path');
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const fullPath = path.isAbsolute(message.filePath) || !workspaceFolder
? message.filePath
: vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath;
vscode.workspace.openTextDocument(fullPath).then(doc => {
vscode.window.showTextDocument(doc);
});
}
break;
case "openFileWithSelection":
// 打开文件并选中代码
if (message.filePath) {
const path = require('path');
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const fullPath = path.isAbsolute(message.filePath) || !workspaceFolder
? message.filePath
: vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath;
vscode.workspace.openTextDocument(fullPath).then(doc => {
vscode.window.showTextDocument(doc).then(editor => {
const start = new vscode.Position(message.startLine - 1, 0);
const end = new vscode.Position(message.endLine - 1, doc.lineAt(message.endLine - 1).text.length);
editor.selection = new vscode.Selection(start, end);
editor.revealRange(new vscode.Range(start, end));
});
});
}
break;
case "openFilePathTag":
// 打开文件路径标签(智能查找)
if (message.filePath) {
const path = require('path');
const fs = require('fs');
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
let fullPath = message.filePath;
// 如果是相对路径且工作区存在
if (!path.isAbsolute(message.filePath) && workspaceFolder) {
const candidatePath = vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath;
// 检查文件是否存在
if (fs.existsSync(candidatePath)) {
fullPath = candidatePath;
} else {
// 尝试在工作区中搜索该文件
const fileName = path.basename(message.filePath);
const files = await vscode.workspace.findFiles(`**/${fileName}`, '**/node_modules/**', 1);
if (files.length > 0) {
fullPath = files[0].fsPath;
}
}
}
if (message.startLine && message.endLine) {
vscode.workspace.openTextDocument(fullPath).then(doc => {
vscode.window.showTextDocument(doc).then(editor => {
const start = new vscode.Position(message.startLine - 1, 0);
const end = new vscode.Position(message.endLine - 1, doc.lineAt(message.endLine - 1).text.length);
editor.selection = new vscode.Selection(start, end);
editor.revealRange(new vscode.Range(start, end));
});
});
} else {
vscode.workspace.openTextDocument(fullPath).then(doc => {
vscode.window.showTextDocument(doc);
});
}
}
break;
case "acceptChange":
// 采纳变更
if (message.changeId) {
@ -503,57 +444,17 @@ export async function showICHelperPanel(
}
break;
case "checkInvitationCode":
// 检查邀请码验证状态
// 【已禁用】检查邀请码验证状态 - 现在所有用户都可以直接使用
{
// 先检查是否是试用用户
const { getCachedUserInfo } = require("../services/userService");
const userInfo = getCachedUserInfo();
if (userInfo?.isPluginTrial === true) {
// 试用用户,跳过邀请码验证,直接返回已验证
console.log("[ICHelperPanel] 试用用户,跳过邀请码验证");
panel.webview.postMessage({
command: "invitationCodeStatus",
verified: true,
});
} else {
// 正式用户,检查邀请码
const {
InvitationService,
} = require("../services/invitationService");
const isVerified = await InvitationService.isVerified(context);
panel.webview.postMessage({
command: "invitationCodeStatus",
verified: isVerified,
});
}
// 直接返回已验证,无需登录和邀请码
panel.webview.postMessage({
command: "invitationCodeStatus",
verified: true,
});
}
break;
case "checkWelcomeModal":
// 检查是否需要显示欢迎弹窗
{
console.log("[ICHelperPanel] 收到 checkWelcomeModal 消息");
const showWelcome = context.globalState.get("showWelcomeModal");
console.log(
"[ICHelperPanel] showWelcomeModal 标记值:",
showWelcome,
);
if (showWelcome) {
// 清除标记并显示欢迎弹窗
await context.globalState.update("showWelcomeModal", undefined);
console.log(
"[ICHelperPanel] ✅ 发送 showWelcomeModal 命令到前端",
);
panel.webview.postMessage({
command: "showWelcomeModal",
});
} else {
console.log(
"[ICHelperPanel] showWelcomeModal 标记为 false不显示弹窗",
);
}
}
// 【已禁用】检查是否需要显示欢迎弹窗 - 无需登录,不显示欢迎弹窗
break;
case "checkTrialExpiration":
// 检查试用期是否过期
@ -568,37 +469,13 @@ export async function showICHelperPanel(
}
break;
case "verifyInvitationCode":
// 验证邀请码
// 【已禁用】验证邀请码 - 无需邀请码验证
{
const {
InvitationService,
} = require("../services/invitationService");
const result = await InvitationService.verifyCode(message.code);
if (result.success) {
// 验证成功,保存状态
await InvitationService.saveVerificationStatus(
context,
message.code,
);
panel.webview.postMessage({
command: "invitationCodeVerified",
success: true,
});
// 延迟显示欢迎弹窗,确保邀请码弹窗已关闭
setTimeout(() => {
panel.webview.postMessage({
command: "showNdtWelcomeModal",
});
}, 300);
} else {
// 验证失败,返回错误信息
panel.webview.postMessage({
command: "invitationCodeVerified",
success: false,
message: result.message,
});
}
// 直接返回验证成功
panel.webview.postMessage({
command: "invitationCodeVerified",
success: true,
});
}
break;
case "openICCoder":
@ -615,7 +492,7 @@ export async function showICHelperPanel(
break;
case "openUserManual":
// 打开用户手册
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
vscode.commands.executeCommand("ic-coder.openUserManual");
break;
case "openUserFeedback":
// 打开用户反馈二维码弹窗
@ -752,6 +629,23 @@ export async function showICHelperPanel(
}
}
break;
// 打开文件
case "openFile":
{
let filePath = message.filePath;
if (filePath) {
// 如果是相对路径,转换为绝对路径
if (!require("path").isAbsolute(filePath)) {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (workspaceFolder) {
filePath = require("path").join(workspaceFolder.uri.fsPath, filePath);
}
}
const uri = vscode.Uri.file(filePath);
vscode.window.showTextDocument(uri);
}
}
break;
// 新增:检查工作区状态
case "checkWorkspace":
const hasWorkspace = !!(
@ -791,6 +685,21 @@ export async function showICHelperPanel(
// 退出登录(前端已有确认对话框)
vscode.commands.executeCommand("ic-coder.logout");
break;
case "saveGeneralSettings":
// 保存通用设置
context.globalState.update('generalSettings', message.settings);
// 更新运行时配置(包括清空)
setCustomConfig({ backendUrl: message.settings.backendUrl || '' });
vscode.window.showInformationMessage('设置已保存');
break;
case "loadGeneralSettings":
// 加载通用设置
const settings = context.globalState.get('generalSettings');
panel.webview.postMessage({
command: 'loadedGeneralSettings',
settings: settings
});
break;
}
},
undefined,

View File

@ -0,0 +1,181 @@
/**
* 用户手册只读预览面板
*/
import * as vscode from "vscode";
export class UserManualPanel {
public static currentPanel: UserManualPanel | undefined;
private readonly _panel: vscode.WebviewPanel;
private _disposables: vscode.Disposable[] = [];
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
this._panel = panel;
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
this._update(extensionUri);
}
public static render(extensionUri: vscode.Uri) {
if (UserManualPanel.currentPanel) {
UserManualPanel.currentPanel._panel.reveal(vscode.ViewColumn.One);
} else {
const panel = vscode.window.createWebviewPanel(
"userManual",
"IC Coder 用户手册",
vscode.ViewColumn.One,
{
enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(extensionUri, "media")],
},
);
UserManualPanel.currentPanel = new UserManualPanel(panel, extensionUri);
}
}
private async _update(extensionUri: vscode.Uri) {
const manualPath = vscode.Uri.joinPath(
extensionUri,
"media",
"USER_MANUAL.md",
);
const markdown = await vscode.workspace.fs.readFile(manualPath);
const content = Buffer.from(markdown).toString("utf-8");
this._panel.webview.html = await this._getHtmlContent(
content,
extensionUri,
);
}
private async _getHtmlContent(
markdown: string,
extensionUri: vscode.Uri,
): Promise<string> {
let inCodeBlock = false;
let inTable = false;
let tableRows: string[] = [];
const lines: string[] = [];
// 先处理图片
markdown = markdown.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => {
const imgUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(extensionUri, "media", src),
);
return `<img src="${imgUri}" alt="${alt}">`;
});
markdown.split("\n").forEach((line) => {
// 代码块
if (line.startsWith("```")) {
if (inCodeBlock) {
lines.push("</code></pre>");
inCodeBlock = false;
} else {
lines.push("<pre><code>");
inCodeBlock = true;
}
return;
}
if (inCodeBlock) {
lines.push(line);
return;
}
// 表格
if (line.startsWith("|")) {
if (!inTable) inTable = true;
tableRows.push(line);
return;
} else if (inTable) {
// 表格结束
const headers = tableRows[0]
.split("|")
.filter((c) => c.trim())
.map((h) => `<th>${h.trim()}</th>`)
.join("");
const body = tableRows
.slice(2)
.map(
(r) =>
"<tr>" +
r
.split("|")
.filter((c) => c.trim())
.map((c) => `<td>${c.trim()}</td>`)
.join("") +
"</tr>",
)
.join("");
lines.push(
`<table><thead><tr>${headers}</tr></thead><tbody>${body}</tbody></table>`,
);
tableRows = [];
inTable = false;
}
// 其他行
if (line === "---") lines.push("<hr>");
else if (line.startsWith("#### "))
lines.push(`<h4>${line.slice(5)}</h4>`);
else if (line.startsWith("### ")) lines.push(`<h3>${line.slice(4)}</h3>`);
else if (line.startsWith("## ")) lines.push(`<h2>${line.slice(3)}</h2>`);
else if (line.startsWith("# ")) lines.push(`<h1>${line.slice(2)}</h1>`);
else if (line.startsWith("- "))
lines.push(
`<li>${line.slice(2).replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")}</li>`,
);
else if (line.trim() === "") lines.push("<p></p>");
else
lines.push(
`<p>${line.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>')}</p>`,
);
});
const html = lines
.join("\n")
.replace(/((?:<li>.*<\/li>\n?)+)/g, "<ul>$1</ul>");
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 40px;
line-height: 1.8;
font-size: 16px;
max-width: 1000px;
margin: 0 auto;
}
h1 { font-size: 2em; border-bottom: 3px solid #ddd; padding-bottom: 15px; margin: 30px 0 20px; }
h2 { font-size: 1.6em; margin-top: 40px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
h3 { font-size: 1.3em; margin-top: 30px; }
h4 { font-size: 1.1em; margin-top: 20px; font-weight: 600; }
p { margin: 15px 0; }
img { display: block; margin: 30px auto; max-width: 100%; border: 1px solid #ddd; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
table { border-collapse: collapse; width: 100%; margin: 25px 0; font-size: 15px; }
th, td { border: 1px solid #ddd; padding: 12px 16px; text-align: left; }
th { background: #636363; font-weight: 600; }
tr:hover { background: #636363; }
ul { margin: 15px 0; padding-left: 30px; }
li { margin: 8px 0; margin-left: 40px;}
pre { background: #2f2f2f; padding: 20px; border-radius: 6px; overflow-x: auto; margin: 20px 0; border: 1px solid #e0e0e0; }
code { font-family: 'Consolas', 'Monaco', monospace; font-size: 14px; line-height: 1.6; }
hr { border: none; border-top: 2px solid #e0e0e0; margin: 30px 0; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
strong { font-weight: 600; color: #e5e5e5; }
</style>
</head>
<body>${html}</body>
</html>`;
}
public dispose() {
UserManualPanel.currentPanel = undefined;
this._panel.dispose();
while (this._disposables.length) {
this._disposables.pop()?.dispose();
}
}
}

View File

@ -0,0 +1,26 @@
/**
* Code Action Provider - 为选中代码提供快捷操作
* 功能:在小灯泡菜单中显示"添加到 IC Coder 对话"选项
*/
import * as vscode from 'vscode';
export class ICCoderCodeActionProvider implements vscode.CodeActionProvider {
provideCodeActions(
document: vscode.TextDocument,
range: vscode.Range
): vscode.CodeAction[] {
const selectedText = document.getText(range);
if (!selectedText) return [];
const action = new vscode.CodeAction(
'💬 添加到 IC Coder 对话',
vscode.CodeActionKind.RefactorRewrite
);
action.command = {
command: 'ic-coder.addCodeToChat',
title: '添加到对话'
};
return [action];
}
}

View File

@ -18,6 +18,12 @@ class ChangeTrackerService {
* 开始新的变更会话
*/
startSession(sessionId: string): void {
// 如果已有 session无论状态重用并重置为 active
if (this.currentSession) {
this.currentSession.status = 'active';
return;
}
this.currentSession = {
sessionId,
startTime: Date.now(),
@ -71,7 +77,6 @@ class ChangeTrackerService {
this.notifyListeners();
return session;
}
this.currentSession = null;
return null;
}

View File

@ -30,7 +30,6 @@ import type {
import { submitToolConfirm, submitAnswer, stopDialog } from "./apiClient";
import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { getUserIdFromToken, isTokenExpired } from "../utils/jwtUtils";
import { updateCachedBalance } from "./creditsService";
/**
* 消息段落类型
@ -43,8 +42,7 @@ export interface MessageSegment {
toolResult?: string;
toolDescription?: string;
askId?: string;
question?: string;
options?: string[];
questions?: import("../types/api").QuestionItem[];
// 智能体相关字段
agentId?: string;
agentName?: string;
@ -97,7 +95,7 @@ export interface DialogCallbacks {
summary: string
) => void;
/** 显示问题ask_user */
onQuestion?: (askId: string, question: string, options: string[]) => void;
onQuestion?: (askId: string, questions: import("../types/api").QuestionItem[]) => void;
/** 实时更新段落(流式过程中) */
onSegmentUpdate?: (segments: MessageSegment[]) => void;
/** 对话完成,返回所有段落 */
@ -130,6 +128,8 @@ export class DialogSession {
private currentTextSegment: MessageSegment | null = null;
private completeCallback: ((segments: MessageSegment[]) => void) | null =
null; // 保存完成回调,用于 abort 时触发
private consecutiveToolErrors = 0; // 连续工具错误计数
private readonly MAX_CONSECUTIVE_ERRORS = 5; // 最大连续错误次数
constructor(extensionPath: string, existingTaskId?: string) {
// 支持复用现有 taskId用于 Plan 模式确认后继续执行)
@ -445,13 +445,17 @@ export class DialogSession {
const expired = isTokenExpired(session.accessToken);
if (expired === true) {
console.error("[DialogSession] token 已过期,需要重新登录");
/*
vscode.window
.showErrorMessage("登录已过期,请重新登录", "重新登录")
.then((selection) => {
if (selection === "重新登录") {
vscode.commands.executeCommand("iccoder.login");
vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
}
});
*/
throw new Error("登录已过期,请重新登录");
}
@ -645,8 +649,11 @@ export class DialogSession {
this.segments.push({
type: "question",
askId: askId,
question: question,
options: ["确认执行", "取消"],
questions: [{
question: question,
options: ["确认执行", "取消"],
multiSelect: false
}],
});
// 实时发送段落更新
@ -664,8 +671,11 @@ export class DialogSession {
await userInteractionManager.handleAskUser(
{
askId: askId,
question: question,
options: ["确认执行", "取消"],
questions: [{
question: question,
options: ["确认执行", "取消"],
multiSelect: false
}]
} as AskUserEvent,
this.taskId
);
@ -712,8 +722,11 @@ export class DialogSession {
// 注册问题到前端(类似 askUser以便用户回答时能找到
const planEvent = {
askId: askId,
question: `请确认执行计划:${data.title}`,
options: ["确认执行", "修改计划", "取消"],
questions: [{
question: `请确认执行计划:${data.title}`,
options: ["确认执行", "修改计划", "取消"],
multiSelect: false
}]
};
try {
await userInteractionManager.handleAskUser(
@ -854,13 +867,12 @@ export class DialogSession {
this.segments.push({
type: "question",
askId: data.askId,
question: data.question,
options: data.options,
questions: data.questions,
});
// 实时发送段落更新(包含问题)
callbacks.onSegmentUpdate?.(this.segments);
// 同时调用 onQuestion 用于更新状态栏等
callbacks.onQuestion?.(data.askId, data.question, data.options);
callbacks.onQuestion?.(data.askId, data.questions);
try {
await userInteractionManager.handleAskUser(data, this.taskId);
} catch (error) {
@ -890,13 +902,17 @@ export class DialogSession {
data.message.includes("LOGIN_EXPIRED") ||
data.message.includes("登录状态已过期")
) {
/*
vscode.window
.showErrorMessage("登录状态已过期,请重新登录", "重新登录")
.then((selection) => {
if (selection === "重新登录") {
vscode.commands.executeCommand("ic-coder.login");
vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
}
});
*/
// 登录过期错误已处理,不再传递给外部
return;
}
@ -1006,8 +1022,9 @@ export class DialogSession {
data.remainingCredits
);
// 更新余额缓存
updateCachedBalance(data.remainingCredits);
// updateCachedBalance(data.remainingCredits);
// 资源点余额低于阈值时弹窗提醒
/*
const LOW_CREDIT_THRESHOLD = 5;
if (data.remainingCredits < LOW_CREDIT_THRESHOLD) {
vscode.window
@ -1019,13 +1036,13 @@ export class DialogSession {
)
.then((selection) => {
if (selection === "去充值") {
// 打开充值页面
vscode.env.openExternal(
vscode.Uri.parse("https://iccoder.com/recharge")
vscode.Uri.parse("https://iccoder.com/memberCenter")
);
}
});
}
*/
},
onOpen: () => {
@ -1106,7 +1123,8 @@ export class DialogSession {
async submitAnswer(
askId: string,
selected?: string[],
customInput?: string
customInput?: string,
answers?: { [questionIndex: string]: string[] }
): Promise<void> {
// 直接调用 receiveAnswer传递 taskId 作为 fallbackTaskId
// 如果 pendingQuestions 中有问题,走正常流程
@ -1115,6 +1133,7 @@ export class DialogSession {
askId,
selected,
customInput,
answers,
this.taskId
);
}

View File

@ -2,7 +2,6 @@ import * as vscode from "vscode";
import * as http from "http";
import * as path from "path";
import * as fs from "fs";
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
import { getConfig } from "../config/settings";
import { resetInvitationVerification } from "./apiClient";
@ -85,7 +84,7 @@ export class ICCoderAuthenticationProvider
const oldSession = this._sessions[0];
this._sessions = [];
await this.saveSessions();
await clearUserInfo();
// await clearUserInfo();
this._onDidChangeSessions.fire({
added: [],
removed: [oldSession],
@ -97,15 +96,15 @@ export class ICCoderAuthenticationProvider
const token = await this.login();
// 获取到 token 后立即调用用户信息接口
const userInfo = await onTokenReceived(token);
// const userInfo = await onTokenReceived(token);
// 创建会话
const session: vscode.AuthenticationSession = {
id: this.generateSessionId(),
accessToken: token,
account: {
id: userInfo?.userId || "iccoder-user",
label: userInfo?.nickname || userInfo?.username || "IC Coder 用户",
id: "user",
label: "IC Coder User",
},
scopes: [...scopes],
};
@ -158,7 +157,7 @@ export class ICCoderAuthenticationProvider
await this.saveSessions();
// 3. 清除用户信息缓存
await clearUserInfo();
// await clearUserInfo();
// 4. 触发会话变化事件
this._onDidChangeSessions.fire({
@ -176,6 +175,28 @@ export class ICCoderAuthenticationProvider
}
}
/**
* Clear local authentication state without window reload.
* Used by re-login flow when session is expired.
*/
async clearSessionsForRelogin(): Promise<void> {
if (this._sessions.length === 0) {
// await clearUserInfo();
return;
}
const removed = [...this._sessions];
this._sessions = [];
await this.saveSessions();
// await clearUserInfo();
this._onDidChangeSessions.fire({
added: [],
removed,
changed: [],
});
}
/**
* 生成会话 ID
*/

View File

@ -161,7 +161,16 @@ export async function startStreamDialog(
const body = JSON.stringify(request);
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, mode=${request.mode}, url=${urlString}`);
console.log('[SSE] 请求详情:', {
url: urlString,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
hasToken: !!request.token,
},
body: request
});
return new Promise((resolve, reject) => {
const options: http.RequestOptions = {

View File

@ -2,23 +2,31 @@
* 工具执行器
* 接收后端的 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 { resolveWorkspaceFilePath, showFileDiff } from '../utils/fileDiff';
import { changeTracker } from './changeTracker';
import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner';
import { analyzeVcdFile } from '../utils/vcdParser';
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
import * as vscode from "vscode";
import * as path from "path";
import * as os from "os";
import * as fs from "fs";
import { readFileContent, readDirectory, listDirectory } from "../utils/readFiles";
import { createOrOverwriteFile } from "../utils/createFiles";
import { resolveWorkspaceFilePath, showFileDiff } from "../utils/fileDiff";
import { changeTracker } from "./changeTracker";
import {
generateVCD,
checkIverilogAvailable,
generateMultiVCD,
DumpModule,
} from "../utils/iverilogRunner";
import { analyzeVcdFile } from "../utils/vcdParser";
import {
executeWaveformTrace,
WaveformTraceArgs,
} from "../utils/waveformTracer";
import {
submitToolResult,
createSuccessResult,
createBusinessErrorResult,
createSystemErrorResult
} from './apiClient';
createSystemErrorResult,
} from "./apiClient";
import type {
ToolCallRequest,
ToolName,
@ -31,8 +39,8 @@ import type {
SimulationArgs,
WaveformSummaryArgs,
KnowledgeSaveArgs,
KnowledgeLoadArgs
} from '../types/api';
KnowledgeLoadArgs,
} from "../types/api";
/**
* 工具执行器上下文
@ -51,7 +59,7 @@ export interface ToolExecutorContext {
*/
export async function executeToolCall(
request: ToolCallRequest,
context: ToolExecutorContext
context: ToolExecutorContext,
): Promise<void> {
const toolName = request.params.name as ToolName;
const args = request.params.arguments;
@ -63,37 +71,53 @@ export async function executeToolCall(
let resultText: string;
switch (toolName) {
case 'file_read':
case "file_read":
resultText = await executeFileRead(args as unknown as FileReadArgs);
break;
case 'file_write':
case "file_write":
resultText = await executeFileWrite(args as unknown as FileWriteArgs);
break;
case 'file_delete':
case "file_delete":
resultText = await executeFileDelete(args as unknown as FileDeleteArgs);
break;
case 'file_list':
case "file_list":
resultText = await executeFileList(args as unknown as FileListArgs);
break;
case 'syntax_check':
resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context);
case "syntax_check":
resultText = await executeSyntaxCheck(
args as unknown as SyntaxCheckArgs,
context,
);
break;
case 'iverilog':
resultText = await executeIverilog(args as unknown as IverilogArgs, context);
case "iverilog":
resultText = await executeIverilog(
args as unknown as IverilogArgs,
context,
);
break;
case 'simulation':
resultText = await executeSimulation(args as unknown as SimulationArgs, context);
case "simulation":
resultText = await executeSimulation(
args as unknown as SimulationArgs,
context,
);
break;
case 'waveform_summary':
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
case "waveform_summary":
resultText = await executeWaveformSummary(
args as unknown as WaveformSummaryArgs,
);
break;
case 'waveform_trace':
resultText = await executeWaveformTrace(args as unknown as WaveformTraceArgs, context);
case "waveform_trace":
resultText = await executeWaveformTrace(
args as unknown as WaveformTraceArgs,
context,
);
break;
case 'knowledge_save':
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs);
case "knowledge_save":
resultText = await executeKnowledgeSave(
args as unknown as KnowledgeSaveArgs,
);
break;
case 'knowledge_load':
case "knowledge_load":
resultText = await executeKnowledgeLoad();
break;
default:
@ -102,16 +126,31 @@ export async function executeToolCall(
// 提交成功结果
const result = createSuccessResult(callId, resultText);
console.log(`[ToolExecutor] 准备提交结果: ${toolName}, callId=${callId}`);
await submitToolResult(result);
console.log(`[ToolExecutor] 工具执行成功: ${toolName}, callId=${callId}`);
console.log(`[ToolExecutor] 结果提交成功: ${toolName}, callId=${callId}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '未知错误';
console.error(`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`, error);
const errorMessage = error instanceof Error ? error.message : "未知错误";
console.error(
`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`,
error,
);
// 提交错误结果
const result = createBusinessErrorResult(callId, errorMessage);
await submitToolResult(result);
try {
const result = createBusinessErrorResult(callId, errorMessage);
console.log(`[ToolExecutor] 准备提交错误结果: ${toolName}, callId=${callId}`);
await submitToolResult(result);
console.log(`[ToolExecutor] 错误结果提交成功: ${toolName}, callId=${callId}`);
} catch (submitError) {
console.error(
`[ToolExecutor] 提交错误结果失败: ${toolName}, callId=${callId}`,
submitError,
);
throw submitError;
}
// 重新抛出原始错误,让调用方知道工具执行失败
throw error;
}
}
@ -129,7 +168,7 @@ async function executeFileRead(args: FileReadArgs): Promise<string> {
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
const absolutePath = resolveWorkspaceFilePath(args.path);
const existedBeforeWrite = fs.existsSync(absolutePath);
const oldContent = existedBeforeWrite ? await readFileContent(args.path) : '';
const oldContent = existedBeforeWrite ? await readFileContent(args.path) : "";
await createOrOverwriteFile(args.path, args.content);
@ -137,11 +176,11 @@ async function executeFileWrite(args: FileWriteArgs): Promise<string> {
try {
changeTracker.trackChange(args.path, oldContent, args.content);
} catch (error) {
console.warn('[ToolExecutor] 记录文件变更失败:', error);
console.warn("[ToolExecutor] 记录文件变更失败:", error);
}
// Verilog 文件添加知识图谱提示
const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv');
const isVerilogFile = args.path.endsWith(".v") || args.path.endsWith(".sv");
if (isVerilogFile) {
return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`;
}
@ -151,7 +190,7 @@ async function executeFileWrite(args: FileWriteArgs): Promise<string> {
/**
* 执行 file_delete 工具
* 删除指定路径的文件
* 删除指定路径的文件(带用户确认)
*/
async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
const filePath = args.path;
@ -159,7 +198,7 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
// 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区');
throw new Error("请先打开一个工作区");
}
const workspacePath = workspaceFolders[0].uri.fsPath;
@ -180,18 +219,60 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
throw new Error(`不能删除目录,请指定文件路径: ${filePath}`);
}
// 验证文件路径在工作区内
const isInWorkspace = workspaceFolders.some((folder) =>
absolutePath.startsWith(folder.uri.fsPath),
);
if (!isInWorkspace) {
throw new Error("只能删除工作区内的文件");
}
// 保护敏感文件
const protectedFiles = [
"package.json",
"tsconfig.json",
".git",
"node_modules",
];
const fileName = path.basename(absolutePath);
if (protectedFiles.includes(fileName)) {
throw new Error(`不允许删除系统文件: ${fileName}`);
}
// 弹出确认对话框
const confirmed = await vscode.window.showWarningMessage(
`确定要删除文件吗?\n\n📄 ${path.basename(filePath)}\n📁 ${path.dirname(filePath)}`,
{
modal: true, // 模态对话框,阻止其他操作
detail: "⚠️ 文件将被移到回收站,可以恢复",
},
"确定删除",
"取消",
);
// 用户取消或关闭对话框
if (confirmed !== "确定删除") {
throw new Error("用户取消了删除操作");
}
// 读取文件内容用于变更追踪
const oldContent = fs.readFileSync(absolutePath, 'utf-8');
const oldContent = fs.readFileSync(absolutePath, "utf-8");
// 记录删除变更
const relativePath = path.relative(workspacePath, absolutePath);
changeTracker.trackChange(relativePath, oldContent, '');
changeTracker.trackChange(relativePath, oldContent, "");
// 删除文件
fs.unlinkSync(absolutePath);
// 删除文件(移到回收站)
const uri = vscode.Uri.file(absolutePath);
await vscode.workspace.fs.delete(uri, {
recursive: false, // 不是目录,设为 false
useTrash: true, // 移到回收站而非永久删除
});
// Verilog 文件添加知识图谱提示
const isVerilogFile = filePath.endsWith('.v') || filePath.endsWith('.sv');
const isVerilogFile = filePath.endsWith(".v") || filePath.endsWith(".sv");
if (isVerilogFile) {
return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`;
}
@ -203,13 +284,11 @@ async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
* 执行 file_list 工具
*/
async function executeFileList(args: FileListArgs): Promise<string> {
const dirPath = args.path || '.';
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 || '(目录为空)';
const files = await listDirectory(dirPath, extensions);
return files.join("\n") || "(目录为空)";
}
/**
@ -218,12 +297,12 @@ async function executeFileList(args: FileListArgs): Promise<string> {
*/
async function executeSyntaxCheck(
args: SyntaxCheckArgs,
context: ToolExecutorContext
context: ToolExecutorContext,
): Promise<string> {
// 检查 iverilog 是否可用
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
if (!iverilogCheck.available) {
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
throw new Error(`IC Coder编译器不可用: ${iverilogCheck.message}`);
}
// 创建临时文件
@ -232,33 +311,33 @@ async function executeSyntaxCheck(
try {
// 写入代码到临时文件
fs.writeFileSync(tempFile, args.code, 'utf-8');
fs.writeFileSync(tempFile, args.code, "utf-8");
// 调用 iverilog 进行语法检查
const { spawn } = require('child_process');
const { spawn } = require("child_process");
const iverilogPath = getIverilogPath(context.extensionPath);
return new Promise((resolve, reject) => {
const child = spawn(iverilogPath, ['-t', 'null', tempFile], {
const child = spawn(iverilogPath, ["-t", "null", tempFile], {
cwd: tempDir,
env: {
...process.env,
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
}
IVERILOG_ROOT: path.join(context.extensionPath, "tools", "iverilog"),
},
});
let stdout = '';
let stderr = '';
let stdout = "";
let stderr = "";
child.stdout.on('data', (data: Buffer) => {
child.stdout.on("data", (data: Buffer) => {
stdout += data.toString();
});
child.stderr.on('data', (data: Buffer) => {
child.stderr.on("data", (data: Buffer) => {
stderr += data.toString();
});
child.on('close', (code: number) => {
child.on("close", (code: number) => {
// 清理临时文件
try {
fs.unlinkSync(tempFile);
@ -267,13 +346,13 @@ async function executeSyntaxCheck(
}
if (code === 0) {
resolve('语法检查通过,无错误。');
resolve("语法检查通过,无错误。");
} else {
resolve(`语法检查发现错误:\n${stderr || stdout}`);
}
});
child.on('error', (error: Error) => {
child.on("error", (error: Error) => {
try {
fs.unlinkSync(tempFile);
} catch (e) {
@ -282,7 +361,6 @@ async function executeSyntaxCheck(
reject(error);
});
});
} catch (error) {
// 确保清理临时文件
try {
@ -300,18 +378,18 @@ async function executeSyntaxCheck(
*/
async function executeIverilog(
args: IverilogArgs,
context: ToolExecutorContext
context: ToolExecutorContext,
): Promise<string> {
// 检查 iverilog 是否可用
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
if (!iverilogCheck.available) {
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
throw new Error(`IC Coder编译器不可用: ${iverilogCheck.message}`);
}
// 获取工作目录
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('没有打开的工作区');
throw new Error("没有打开的工作区");
}
const projectPath = workspaceFolders[0].uri.fsPath;
const workDir = args.workDir
@ -320,32 +398,32 @@ async function executeIverilog(
// 解析参数
const iverilogPath = getIverilogPath(context.extensionPath);
const cmdArgs = args.args.split(/\s+/).filter(a => a.length > 0);
const cmdArgs = args.args.split(/\s+/).filter((a) => a.length > 0);
const { spawn } = require('child_process');
const { spawn } = require("child_process");
return new Promise((resolve, reject) => {
const child = spawn(iverilogPath, cmdArgs, {
cwd: workDir,
env: {
...process.env,
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
}
IVERILOG_ROOT: path.join(context.extensionPath, "tools", "iverilog"),
},
});
let stdout = '';
let stderr = '';
let stdout = "";
let stderr = "";
child.stdout.on('data', (data: Buffer) => {
child.stdout.on("data", (data: Buffer) => {
stdout += data.toString();
});
child.stderr.on('data', (data: Buffer) => {
child.stderr.on("data", (data: Buffer) => {
stderr += data.toString();
});
child.on('close', (code: number) => {
const output = stderr || stdout || '(无输出)';
child.on("close", (code: number) => {
const output = stderr || stdout || "(无输出)";
if (code === 0) {
resolve(`执行成功\n${output}`);
} else {
@ -353,7 +431,7 @@ async function executeIverilog(
}
});
child.on('error', (error: Error) => {
child.on("error", (error: Error) => {
reject(error);
});
});
@ -364,12 +442,12 @@ async function executeIverilog(
*/
async function executeSimulation(
args: SimulationArgs,
context: ToolExecutorContext
context: ToolExecutorContext,
): Promise<string> {
// 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区');
throw new Error("请先打开一个工作区");
}
const projectPath = workspaceFolders[0].uri.fsPath;
@ -377,21 +455,24 @@ async function executeSimulation(
// 检查是否有 dumpModules 参数(多 VCD 模式)
if (args.dumpModules) {
const modules = parseDumpModules(args.dumpModules);
const vcdDir = args.vcdDir || 'vcd';
const vcdDir = args.vcdDir || "vcd";
const result = await generateMultiVCD(
projectPath,
context.extensionPath,
args.tbPath,
modules,
vcdDir
vcdDir,
);
if (result.success) {
const vcdList = result.vcdFiles
.map(f => `- ${f.moduleName}: ${f.success ? f.vcdPath : '失败 - ' + f.error}`)
.join('\n');
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? '\n\n仿真输出:' + result.stdout : ''}`;
.map(
(f) =>
`- ${f.moduleName}: ${f.success ? f.vcdPath : "失败 - " + f.error}`,
)
.join("\n");
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? "\n\n仿真输出:" + result.stdout : ""}`;
} else {
throw new Error(result.message);
}
@ -420,8 +501,8 @@ async function executeSimulation(
* 格式name:path,name:path
*/
function parseDumpModules(dumpModules: string): DumpModule[] {
return dumpModules.split(',').map(item => {
const [name, modulePath] = item.trim().split(':');
return dumpModules.split(",").map((item) => {
const [name, modulePath] = item.trim().split(":");
return { name: name.trim(), path: modulePath.trim() };
});
}
@ -430,13 +511,15 @@ function parseDumpModules(dumpModules: string): DumpModule[] {
* 执行 waveform_summary 工具
* 解析 VCD 文件并返回波形摘要
*/
async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string> {
async function executeWaveformSummary(
args: WaveformSummaryArgs,
): Promise<string> {
const { vcdPath, signals, checkpoints } = args;
// 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区');
throw new Error("请先打开一个工作区");
}
const workspacePath = workspaceFolders[0].uri.fsPath;
@ -467,17 +550,20 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
const workspaceFolder = getWorkspaceFolder();
if (!workspaceFolder) {
throw new Error('请先打开一个工作区');
throw new Error("请先打开一个工作区");
}
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder');
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, 'knowledge.json');
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, ".iccoder");
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, "knowledge.json");
// 确保 .iccoder 目录存在(兼容远程/虚拟工作区)
await vscode.workspace.fs.createDirectory(iccoderDirUri);
// 写入知识图谱UTF-8
await vscode.workspace.fs.writeFile(knowledgeUri, Buffer.from(args.data || '', 'utf-8'));
await vscode.workspace.fs.writeFile(
knowledgeUri,
Buffer.from(args.data || "", "utf-8"),
);
return `知识图谱已保存: .iccoder/knowledge.json`;
}
@ -489,20 +575,33 @@ async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
async function executeKnowledgeLoad(): Promise<string> {
const workspaceFolder = getWorkspaceFolder();
if (!workspaceFolder) {
throw new Error('请先打开一个工作区');
throw new Error("请先打开一个工作区");
}
const knowledgeUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder', 'knowledge.json');
const knowledgeUri = vscode.Uri.joinPath(
workspaceFolder.uri,
".iccoder",
"knowledge.json",
);
try {
const bytes = await vscode.workspace.fs.readFile(knowledgeUri);
const content = Buffer.from(bytes).toString('utf-8');
const content = Buffer.from(bytes).toString("utf-8");
return content;
} catch (error) {
// 文件不存在:返回空图谱
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') {
if (
error instanceof vscode.FileSystemError &&
error.code === "FileNotFound"
) {
// 与后端 KnowledgeGraph 结构保持一致nodes/edges + nodeClass 多态字段)
return JSON.stringify({ taskId: '', version: 1, module: null, nodes: [], edges: [] });
return JSON.stringify({
taskId: "",
version: 1,
module: null,
nodes: [],
edges: [],
});
}
throw error;
}
@ -515,7 +614,9 @@ function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
}
const activeUri = vscode.window.activeTextEditor?.document?.uri;
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined;
const activeFolder = activeUri
? vscode.workspace.getWorkspaceFolder(activeUri)
: undefined;
return activeFolder ?? folders[0];
}
@ -524,22 +625,24 @@ function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
*/
function getIverilogPath(extensionPath: string): string {
const platform = process.platform;
if (platform === 'win32') {
return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog.exe');
if (platform === "win32") {
return path.join(extensionPath, "tools", "iverilog", "bin", "iverilog.exe");
} else {
return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog');
return path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
}
}
/**
* 创建工具执行器上下文
*/
export function createToolExecutorContext(extensionPath: string): ToolExecutorContext {
export function createToolExecutorContext(
extensionPath: string,
): ToolExecutorContext {
const workspaceFolders = vscode.workspace.workspaceFolders;
const workspacePath = workspaceFolders?.[0]?.uri.fsPath || '';
const workspacePath = workspaceFolders?.[0]?.uri.fsPath || "";
return {
extensionPath,
workspacePath
workspacePath,
};
}

View File

@ -4,7 +4,7 @@
*/
import * as vscode from 'vscode';
import { submitAnswer, submitToolConfirm } from './apiClient';
import type { AskUserEvent, AnswerRequest } from '../types/api';
import type { AskUserEvent, AnswerRequest, QuestionItem } from '../types/api';
/**
* 待处理的用户问题
@ -12,8 +12,7 @@ import type { AskUserEvent, AnswerRequest } from '../types/api';
interface PendingQuestion {
askId: string;
taskId: string;
question: string;
options: string[];
questions: QuestionItem[];
resolve: (answer: string) => void;
reject: (error: Error) => void;
}
@ -45,9 +44,9 @@ export class UserInteractionManager {
* @param taskId 当前任务ID
*/
async handleAskUser(event: AskUserEvent, taskId: string): Promise<void> {
const { askId, question, options } = event;
const { askId, questions } = event;
console.log(`[UserInteraction] 收到问题: askId=${askId}, question=${question}`);
console.log(`[UserInteraction] 收到问题: askId=${askId}, count=${questions.length}`);
// 注意:问题显示已经通过 dialogService 的 onSegmentUpdate 统一处理
// 这里不再单独发送 showQuestion 命令,避免重复显示
@ -57,8 +56,7 @@ export class UserInteractionManager {
this.pendingQuestions.set(askId, {
askId,
taskId,
question,
options,
questions,
resolve: (answer: string) => {
this.submitUserAnswer(askId, taskId, answer)
.then(() => resolve())
@ -80,24 +78,38 @@ export class UserInteractionManager {
/**
* 处理用户提交的回答(从 WebView 调用)
* @param askId 问题ID
* @param selected 选中的选项
* @param customInput 自定义输入
* @param selected 选中的选项(旧格式)
* @param customInput 自定义输入(旧格式)
* @param answers 新格式:按问题索引的答案
* @param fallbackTaskId 当问题不存在时使用的 taskId用于直接发送到后端
*/
async receiveAnswer(
askId: string,
selected?: string[],
customInput?: string,
answers?: { [questionIndex: string]: string[] },
fallbackTaskId?: string
): Promise<void> {
const pending = this.pendingQuestions.get(askId);
const answer = customInput || selected?.join(', ') || '';
// 构建答案字符串
let answer = '';
if (answers && Object.keys(answers).length > 0) {
// 新格式:多问题答案
answer = Object.entries(answers)
.sort(([a], [b]) => parseInt(a) - parseInt(b))
.map(([_, vals]) => vals.join('; '))
.join(' | ');
} else {
// 旧格式:单问题答案
answer = customInput || selected?.join(', ') || '';
}
if (!pending) {
// 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端
if (fallbackTaskId) {
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`);
await this.submitUserAnswer(askId, fallbackTaskId, answer);
await this.submitUserAnswer(askId, fallbackTaskId, answer, answers);
} else {
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
}
@ -119,7 +131,8 @@ export class UserInteractionManager {
private async submitUserAnswer(
askId: string,
taskId: string,
answer: string
answer: string,
answers?: { [questionIndex: string]: string[] }
): Promise<void> {
// 检查是否是工具确认类型的问题
if (askId.startsWith('tool_confirm_')) {
@ -148,7 +161,8 @@ export class UserInteractionManager {
const request: AnswerRequest = {
askId,
taskId,
customInput: answer
answers: answers,
customInput: answers ? undefined : answer
};
try {

View File

@ -162,10 +162,14 @@ export async function getUserInfo(token: string): Promise<UserInfo | null> {
console.log('[UserService] 从 getInfo 接口获取到 isPluginTrial: true');
}
// 获取试用到期时间
if (response.enterpriseTrialExpires) {
// 获取试用到期时间null 表示长期有效)
if (response.enterpriseTrialExpires !== undefined) {
userInfo.pluginTrialExpiresAt = response.enterpriseTrialExpires;
console.log('[UserService] 试用到期时间:', new Date(response.enterpriseTrialExpires).toLocaleString());
if (response.enterpriseTrialExpires === null) {
console.log('[UserService] 试用长期有效');
} else {
console.log('[UserService] 试用到期时间:', new Date(response.enterpriseTrialExpires).toLocaleString());
}
}
return userInfo;
@ -331,33 +335,21 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
// 保存到持久化存储
await saveUserInfo(userInfo);
// 判断是否是插件试用用户
console.log('[UserService] 检查用户类型isPluginTrial:', userInfo.isPluginTrial);
console.log('[UserService] extensionContext 是否存在:', !!extensionContext);
if (userInfo.isPluginTrial === true) {
// 插件试用用户:标记需要显示欢迎弹窗
const hasWelcomed = extensionContext?.globalState.get('pluginTrialWelcomed');
console.log('[UserService] 是否已显示过欢迎弹窗:', hasWelcomed);
if (!hasWelcomed && extensionContext) {
// 设置标记,让聊天面板显示欢迎弹窗
await extensionContext.globalState.update('showWelcomeModal', true);
await extensionContext.globalState.update('pluginTrialWelcomed', true);
console.log('[UserService] ✅ 已设置欢迎弹窗标记 showWelcomeModal=true');
// 验证标记是否设置成功
const checkMark = extensionContext.globalState.get('showWelcomeModal');
console.log('[UserService] 验证标记:', checkMark);
} else if (!extensionContext) {
console.error('[UserService] ❌ extensionContext 为 null无法设置标记');
} else {
console.log('[UserService] 已经显示过欢迎弹窗,跳过');
}
} else {
// 正式用户:显示邀请码弹窗(现有逻辑)
console.log('[UserService] 正式用户登录,将在面板中检查邀请码');
}
// 【已禁用】试用用户和欢迎弹窗逻辑 - 无需登录
// if (userInfo.isPluginTrial === true && userInfo.pluginTrialExpiresAt !== null && userInfo.pluginTrialExpiresAt !== undefined) {
// const now = Date.now();
// const isExpired = now >= userInfo.pluginTrialExpiresAt;
// if (isExpired) {
// console.log('[UserService] 试用已过期,将显示邀请码弹窗');
// } else {
// const hasWelcomed = extensionContext?.globalState.get('pluginTrialWelcomed');
// if (!hasWelcomed && extensionContext) {
// await extensionContext.globalState.update('showWelcomeModal', true);
// await extensionContext.globalState.update('pluginTrialWelcomed', true);
// console.log('[UserService] ✅ 已设置欢迎弹窗标记 showWelcomeModal=true');
// }
// }
// }
return userInfo;
} catch (error) {

View File

@ -194,11 +194,17 @@ export interface PlanSummaryUpdateEvent {
timestamp: number;
}
/** 单个问题项 */
export interface QuestionItem {
question: string;
options: string[];
multiSelect?: boolean;
}
/** ask_user 事件数据 */
export interface AskUserEvent {
askId: string;
question: string;
options: string[];
questions: QuestionItem[];
}
/** complete 事件数据 */
@ -351,10 +357,12 @@ export interface AnswerRequest {
askId: string;
/** 任务ID */
taskId: string;
/** 选中的选项列表 */
/** 选中的选项列表(旧格式,兼容) */
selected?: string[];
/** 自定义输入内容 */
/** 自定义输入内容(旧格式,兼容) */
customInput?: string;
/** 新格式:按问题索引的答案 */
answers?: { [questionIndex: string]: string[] };
}
/** 用户回答响应 */

View File

@ -7,7 +7,7 @@ import { promisify } from "util";
function execCommand(
command: string,
args: string[],
options: { cwd: string; env?: any }
options: { cwd: string; env?: any },
): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
// 在 Windows 上,如果路径包含空格,不使用 shell直接用 spawn
@ -23,25 +23,25 @@ function execCommand(
let stderr = "";
// 在 Windows 上使用 GBK 编码解码输出
const encoding = process.platform === 'win32' ? 'gbk' : 'utf8';
const encoding = process.platform === "win32" ? "gbk" : "utf8";
child.stdout.on("data", (data) => {
try {
// 尝试使用 iconv-lite 解码(如果可用)
const iconv = require('iconv-lite');
const iconv = require("iconv-lite");
stdout += iconv.decode(data, encoding);
} catch {
// 如果 iconv-lite 不可用,使用默认解码
stdout += data.toString('utf8');
stdout += data.toString("utf8");
}
});
child.stderr.on("data", (data) => {
try {
const iconv = require('iconv-lite');
const iconv = require("iconv-lite");
stderr += iconv.decode(data, encoding);
} catch {
stderr += data.toString('utf8');
stderr += data.toString("utf8");
}
});
@ -93,7 +93,7 @@ export interface VCDGenerationResult {
* 检查项目中的 Verilog 文件完整性
*/
export async function checkVerilogProject(
projectPath: string
projectPath: string,
): Promise<VerilogProjectCheck> {
const result: VerilogProjectCheck = {
isComplete: false,
@ -164,7 +164,7 @@ export async function checkVerilogProject(
return result;
} catch (error) {
result.errors.push(
`检查项目时出错: ${error instanceof Error ? error.message : "未知错误"}`
`检查项目时出错: ${error instanceof Error ? error.message : "未知错误"}`,
);
return result;
}
@ -201,6 +201,33 @@ async function findVerilogFiles(dir: string): Promise<string[]> {
return verilogFiles;
}
/**
* 递归查找目录下所有 VCD 文件
*/
async function findVcdFilesRecursive(dir: string): Promise<string[]> {
const vcdFiles: 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) {
if (!fileName.startsWith(".") && fileName !== "node_modules") {
await searchDir(filePath);
}
} else if (fileType === vscode.FileType.File && fileName.endsWith(".vcd")) {
vcdFiles.push(filePath);
}
}
}
await searchDir(dir);
return vcdFiles;
}
/**
* 获取 iverilog 可执行文件路径
*/
@ -209,12 +236,30 @@ async function getIverilogPath(extensionPath: string): Promise<string> {
let iverilogBin = "";
if (platform === "win32") {
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog.exe");
iverilogBin = path.join(
extensionPath,
"tools",
"iverilog",
"bin",
"iverilog.exe",
);
} else if (platform === "darwin") {
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
iverilogBin = path.join(
extensionPath,
"tools",
"iverilog",
"bin",
"iverilog",
);
} else {
// Linux
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
iverilogBin = path.join(
extensionPath,
"tools",
"iverilog",
"bin",
"iverilog",
);
}
// 如果插件包中没有,尝试使用系统安装的 iverilog
@ -258,7 +303,7 @@ async function getVvpPath(extensionPath: string): Promise<string> {
*/
export async function generateVCD(
projectPath: string,
extensionPath: string
extensionPath: string,
): Promise<VCDGenerationResult> {
try {
// 1. 检查项目完整性
@ -286,8 +331,8 @@ export async function generateVCD(
IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"),
};
// 5. 构建 iverilog 编译参数
const compileArgs = ["-o", outputFile, ...projectCheck.allVerilogFiles];
// 5. 构建 iverilog 编译参数(启用 SystemVerilog 2012 标准)
const compileArgs = ["-g2012", "-o", outputFile, ...projectCheck.allVerilogFiles];
console.log("执行编译命令:", iverilogPath, compileArgs.join(" "));
console.log("IVERILOG_ROOT:", env.IVERILOG_ROOT);
@ -299,15 +344,97 @@ export async function generateVCD(
cwd: projectPath,
env: env,
});
console.log("编译成功stdout:", compileResult.stdout);
console.log("编译成功stderr:", compileResult.stderr);
} catch (error: any) {
console.error("编译失败:", error);
return {
success: false,
message: `iverilog 编译失败:\n${error.message}`,
message: `IC Coder编译器编译失败:\n${error.message}`,
stderr: error.stderr,
stdout: error.stdout,
};
}
// 6.1 检查 .vvp 文件是否生成
const fs = require("fs");
if (!fs.existsSync(outputFile)) {
return {
success: false,
message: `编译未生成 .vvp 文件: ${outputFile}`,
stderr: compileResult.stderr,
stdout: compileResult.stdout,
};
}
console.log("已生成 .vvp 文件:", outputFile);
// 6.5. 删除 shebang 行(修复 Windows 上的 vvp 解析错误)
try {
const vvpContent = fs.readFileSync(outputFile, "utf8");
const lines = vvpContent.split("\n");
if (lines.length > 0 && lines[0].startsWith("#!")) {
const cleanedContent = lines.slice(1).join("\n");
fs.writeFileSync(outputFile, cleanedContent, "utf8");
console.log("已删除 .vvp 文件的 shebang 行");
} else {
console.log(".vvp 文件无 shebang 行,跳过");
}
} catch (error) {
console.error("删除 shebang 失败:", error);
return {
success: false,
message: `处理 .vvp 文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
};
}
// 6.6. 检查并创建 VCD 输出目录,并处理 Windows 路径问题
try {
const tbPath = projectCheck.testbenchFile;
if (tbPath && fs.existsSync(tbPath)) {
const tbContent = fs.readFileSync(tbPath, "utf8");
const dumpfileMatch = tbContent.match(/\$dumpfile\s*\(\s*["']([^"']+)["']\s*\)/);
if (dumpfileMatch) {
const vcdPath = dumpfileMatch[1];
const vcdDir = path.dirname(vcdPath);
console.log(`testbench 中的 VCD 路径: ${vcdPath}`);
if (vcdDir && vcdDir !== "." && vcdDir !== "") {
const vcdDirPath = path.join(projectPath, vcdDir);
console.log(`检查 VCD 目录: ${vcdDirPath}`);
if (!fs.existsSync(vcdDirPath)) {
fs.mkdirSync(vcdDirPath, { recursive: true });
console.log(`已创建 VCD 输出目录: ${vcdDirPath}`);
} else {
console.log(`VCD 目录已存在: ${vcdDirPath}`);
}
// Windows 兼容性:修改 .vvp 文件中的路径,将正斜杠替换为反斜杠
if (process.platform === "win32" && vcdPath.includes("/")) {
const vvpContent = fs.readFileSync(outputFile, "utf8");
const windowsPath = vcdPath.replace(/\//g, "\\\\");
const modifiedContent = vvpContent.replace(
new RegExp(`"${vcdPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`, 'g'),
`"${windowsPath}"`
);
fs.writeFileSync(outputFile, modifiedContent, "utf8");
console.log(`已修正 VCD 路径: ${vcdPath} -> ${windowsPath}`);
}
} else {
console.log("VCD 文件在根目录,无需创建子目录");
}
} else {
console.warn("testbench 中未找到 $dumpfile 语句");
}
}
} catch (error) {
console.error("处理 VCD 路径失败:", error);
return {
success: false,
message: `处理 VCD 路径失败: ${error instanceof Error ? error.message : "未知错误"}`,
};
}
// 7. 执行仿真生成 VCD
const simArgs = [outputFile];
console.log("执行仿真命令:", vvpPath, simArgs.join(" "));
@ -318,7 +445,11 @@ export async function generateVCD(
cwd: projectPath,
env: env,
});
console.log("仿真执行完成");
console.log("仿真 stdout:", simResult.stdout);
console.log("仿真 stderr:", simResult.stderr);
} catch (error: any) {
console.error("仿真失败:", error);
return {
success: false,
message: `VVP 仿真失败:\n${error.message}`,
@ -328,23 +459,46 @@ export async function generateVCD(
}
// 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);
let vcdFile: string | null = null;
if (vcdFiles.length === 0) {
// 8.1 尝试从 testbench 中提取 VCD 路径
try {
const fs = require("fs");
const tbPath = projectCheck.testbenchFile;
if (tbPath && fs.existsSync(tbPath)) {
const tbContent = fs.readFileSync(tbPath, "utf8");
const dumpfileMatch = tbContent.match(/\$dumpfile\s*\(\s*["']([^"']+)["']\s*\)/);
if (dumpfileMatch) {
const vcdPath = dumpfileMatch[1];
const absoluteVcdPath = path.join(projectPath, vcdPath);
if (fs.existsSync(absoluteVcdPath)) {
vcdFile = absoluteVcdPath;
console.log(`找到 VCD 文件(从 testbench: ${vcdFile}`);
}
}
}
} catch (error) {
console.warn("从 testbench 提取 VCD 路径失败:", error);
}
// 8.2 如果未找到,递归搜索项目目录
if (!vcdFile) {
const foundFiles = await findVcdFilesRecursive(projectPath);
if (foundFiles.length > 0) {
vcdFile = foundFiles[0];
console.log(`找到 VCD 文件(递归搜索): ${vcdFile}`);
}
}
if (!vcdFile) {
return {
success: false,
message: "VCD 文件未生成。请确保 testbench 中包含 $dumpfile 和 $dumpvars 语句。",
message:
"VCD 文件未生成。请确保 testbench 中包含 $dumpfile 和 $dumpvars 语句。",
stdout: simResult.stdout,
};
}
// 使用找到的第一个 VCD 文件
const vcdFile = path.join(projectPath, vcdFiles[0]);
// 9. 清理中间文件
try {
const outputUri = vscode.Uri.file(outputFile);
@ -373,7 +527,7 @@ export async function generateVCD(
* 检查 iverilog 是否可用
*/
export async function checkIverilogAvailable(
extensionPath: string
extensionPath: string,
): Promise<{ available: boolean; version?: string; message: string }> {
try {
const iverilogPath = await getIverilogPath(extensionPath);
@ -385,7 +539,7 @@ export async function checkIverilogAvailable(
} catch (error) {
return {
available: false,
message: `iverilog 不可用。未找到文件: ${iverilogPath}`,
message: `IC Coder编译器不可用。未找到文件: ${iverilogPath}`,
};
}
@ -404,12 +558,12 @@ export async function checkIverilogAvailable(
return {
available: true,
version: version,
message: `iverilog 可用: ${version}`,
message: `IC Coder编译器可用: ${version}`,
};
} catch (error: any) {
return {
available: false,
message: `iverilog 执行失败: ${error.message}\n${error.stderr || ""}`,
message: `IC Coder编译器执行失败: ${error.message}\n${error.stderr || ""}`,
};
}
}
@ -418,8 +572,8 @@ export async function checkIverilogAvailable(
* 要 dump 的模块定义
*/
export interface DumpModule {
name: string; // 模块名(用于 VCD 文件名和宏名)
path: string; // 实例路径(如 dut.u_tx
name: string; // 模块名(用于 VCD 文件名和宏名)
path: string; // 实例路径(如 dut.u_tx
}
/**
@ -444,10 +598,11 @@ export interface MultiVCDResult {
function injectConditionalDump(
tbContent: string,
dumpModules: DumpModule[],
vcdDir: string
vcdDir: string,
): string {
// 匹配 $dumpfile 和 $dumpvars 语句(可能跨多行)
const dumpPattern = /(\$dumpfile\s*\([^)]+\)\s*;[\s\S]*?\$dumpvars\s*\([^)]+\)\s*;)/g;
const dumpPattern =
/(\$dumpfile\s*\([^)]+\)\s*;[\s\S]*?\$dumpvars\s*\([^)]+\)\s*;)/g;
// 生成条件编译代码
const conditionalCode = generateConditionalDumpCode(dumpModules, vcdDir);
@ -469,7 +624,7 @@ function injectConditionalDump(
*/
function generateConditionalDumpCode(
dumpModules: DumpModule[],
vcdDir: string
vcdDir: string,
): string {
if (dumpModules.length === 0) {
return '$dumpfile("output.vcd");\n $dumpvars(0, dut);';
@ -480,7 +635,7 @@ function generateConditionalDumpCode(
dumpModules.forEach((module, index) => {
const macroName = `DUMP_${module.name.toUpperCase()}`;
const vcdPath = `${vcdDir}/${module.name}.vcd`;
const directive = index === 0 ? '`ifdef' : '`elsif';
const directive = index === 0 ? "`ifdef" : "`elsif";
lines.push(`${directive} ${macroName}`);
lines.push(` $dumpfile("${vcdPath}");`);
@ -488,12 +643,12 @@ function generateConditionalDumpCode(
});
// 添加默认分支(使用第一个模块)
lines.push('`else');
lines.push("`else");
lines.push(` $dumpfile("${vcdDir}/${dumpModules[0].name}.vcd");`);
lines.push(` $dumpvars(1, ${dumpModules[0].path});`);
lines.push('`endif');
lines.push("`endif");
return lines.join('\n');
return lines.join("\n");
}
/**
@ -504,10 +659,10 @@ export async function generateMultiVCD(
extensionPath: string,
tbPath: string,
dumpModules: DumpModule[],
vcdDir: string = 'vcd'
vcdDir: string = "vcd",
): Promise<MultiVCDResult> {
const results: MultiVCDResult['vcdFiles'] = [];
let allStdout = '';
const results: MultiVCDResult["vcdFiles"] = [];
let allStdout = "";
try {
// 1. 创建 vcd 目录
@ -520,16 +675,21 @@ export async function generateMultiVCD(
}
// 2. 读取原始 testbench
const tbFullPath = path.isAbsolute(tbPath) ? tbPath : path.join(projectPath, tbPath);
const tbFullPath = path.isAbsolute(tbPath)
? tbPath
: path.join(projectPath, tbPath);
const tbUri = vscode.Uri.file(tbFullPath);
const tbBytes = await vscode.workspace.fs.readFile(tbUri);
const originalTb = Buffer.from(tbBytes).toString('utf-8');
const originalTb = Buffer.from(tbBytes).toString("utf-8");
// 3. 注入条件编译代码
const modifiedTb = injectConditionalDump(originalTb, dumpModules, vcdDir);
await vscode.workspace.fs.writeFile(tbUri, Buffer.from(modifiedTb, 'utf-8'));
await vscode.workspace.fs.writeFile(
tbUri,
Buffer.from(modifiedTb, "utf-8"),
);
console.log('[generateMultiVCD] Testbench 已修改,开始多次仿真...');
console.log("[generateMultiVCD] Testbench 已修改,开始多次仿真...");
// 4. 获取工具路径
const iverilogPath = await getIverilogPath(extensionPath);
@ -551,30 +711,38 @@ export async function generateMultiVCD(
console.log(`[generateMultiVCD] 仿真模块: ${module.name} (${macroName})`);
try {
// 编译(带宏定义)
// 编译(带宏定义,启用 SystemVerilog 2012 标准
const compileArgs = [
"-g2012",
`-D${macroName}`,
"-o", outputFile,
...projectCheck.allVerilogFiles
"-o",
outputFile,
...projectCheck.allVerilogFiles,
];
await execCommand(iverilogPath, compileArgs, { cwd: projectPath, env });
// 仿真
const simResult = await execCommand(vvpPath, [outputFile], { cwd: projectPath, env });
const simResult = await execCommand(vvpPath, [outputFile], {
cwd: projectPath,
env,
});
allStdout += `\n[${module.name}] ${simResult.stdout}`;
results.push({
moduleName: module.name,
vcdPath: vcdPath,
success: true
success: true,
});
} catch (error: any) {
console.error(`[generateMultiVCD] 模块 ${module.name} 仿真失败:`, error.message);
console.error(
`[generateMultiVCD] 模块 ${module.name} 仿真失败:`,
error.message,
);
results.push({
moduleName: module.name,
vcdPath: vcdPath,
success: false,
error: error.message
error: error.message,
});
// 继续执行其他模块
}
@ -587,19 +755,18 @@ export async function generateMultiVCD(
// 忽略
}
const successCount = results.filter(r => r.success).length;
const successCount = results.filter((r) => r.success).length;
return {
success: successCount > 0,
vcdFiles: results,
message: `生成完成:${successCount}/${dumpModules.length} 个 VCD 文件成功`,
stdout: allStdout
stdout: allStdout,
};
} catch (error) {
return {
success: false,
vcdFiles: results,
message: `生成多 VCD 文件失败: ${error instanceof Error ? error.message : '未知错误'}`
message: `生成多 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
};
}
}

View File

@ -19,10 +19,6 @@ import { dialogManager, DialogSession } from "../services/dialogService";
import { userInteractionManager } from "../services/userInteraction";
import { healthCheck } from "../services/apiClient";
import { isTokenExpired } from "./jwtUtils";
import {
checkBalanceBeforeSend,
fetchBalance,
} from "../services/creditsService";
import { optimizePrompt } from "../services/promptOptimizeService";
import { NotificationService } from "../services/notificationService";
import { TrialExpirationService } from "../services/trialExpirationService";
@ -41,7 +37,14 @@ let currentSession: DialogSession | null = null;
/** 最后一个活跃的 taskId用于压缩等操作 */
let lastTaskId: string | null = null;
async function trackFileChange(filePath: string, oldContent: string, newContent: string): Promise<void> {
/** 离线模式仿真模拟标志(防止重复触发) */
let offlineSimulationTriggered = false;
async function trackFileChange(
filePath: string,
oldContent: string,
newContent: string,
): Promise<void> {
try {
changeTracker.trackChange(filePath, oldContent, newContent);
} catch (error) {
@ -58,17 +61,19 @@ export async function handleUserMessage(
extensionPath?: string,
mode?: RunMode,
serviceTier?: ServiceTier, // 服务等级参数
contextItems?: Array<{ id: number; type: string; path: string }> // 上下文项参数
contextItems?: Array<{ id: number; type: string; path: string }>, // 上下文项参数
) {
console.log("收到用户消息:", text);
// 检查 token 是否过期
// 【已禁用】检查 token 是否过期 - 无需登录
const context = (panel as any).__context;
if (context) {
if (false && context) {
// 从 session 中获取 token
let token: string | undefined;
try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
const session = await vscode.authentication.getSession("iccoder", [], {
createIfNone: false,
});
token = session?.accessToken;
} catch (error) {
console.warn("[MessageHandler] 获取 session 失败:", error);
@ -78,21 +83,23 @@ export async function handleUserMessage(
console.warn("[MessageHandler] 未登录,阻止发送");
// 保存待发送的消息
await context.globalState.update('pendingMessage', {
await context.globalState.update("pendingMessage", {
text,
mode,
serviceTier,
timestamp: Date.now()
timestamp: Date.now(),
});
// 显示弹窗提示
const action = await vscode.window.showWarningMessage(
'请先登录后再发送消息',
'立即登录'
"请先登录后再发送消息",
"立即登录",
);
if (action === '立即登录') {
vscode.commands.executeCommand("ic-coder.login");
if (action === "立即登录") {
vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
}
// 恢复输入状态
@ -104,30 +111,34 @@ export async function handleUserMessage(
return;
}
if (isTokenExpired(token)) {
if (token && isTokenExpired(token as string)) {
console.warn("[MessageHandler] Token 已过期,阻止发送");
// 保存待发送的消息
await context.globalState.update('pendingMessage', {
await context.globalState.update("pendingMessage", {
text,
mode,
serviceTier,
timestamp: Date.now()
timestamp: Date.now(),
});
// 清除过期的 session
await context.globalState.update('icCoderSessions', []);
await context.globalState.update('icCoderUserInfo', undefined);
await context.globalState.update("icCoderSessions", []);
await context.globalState.update("icCoderUserInfo", undefined);
// 显示弹窗提示
/*
const action = await vscode.window.showWarningMessage(
'登录已过期,请重新登录',
'立即登录'
"登录已过期,请重新登录",
"立即登录",
);
if (action === '立即登录') {
vscode.commands.executeCommand("ic-coder.login");
if (action === "立即登录") {
vscode.commands.executeCommand("ic-coder.login", {
forceReauth: true,
});
}
*/
// 恢复输入状态
panel.webview.postMessage({
@ -179,29 +190,6 @@ export async function handleUserMessage(
return;
}
// 发送前检测余额
const balanceCheck = await checkBalanceBeforeSend();
if (!balanceCheck.allowed) {
console.warn("[MessageHandler] 余额不足,阻止发送:", balanceCheck.message);
// 显示错误提示
const selection = await vscode.window.showWarningMessage(
balanceCheck.message || "资源点余额不足",
"去充值"
);
if (selection === "去充值") {
vscode.env.openExternal(
vscode.Uri.parse("https://iccoder.com/memberCenter")
);
}
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
return;
}
// 尝试使用后端服务
if (useBackendService && extensionPath) {
try {
@ -212,14 +200,14 @@ export async function handleUserMessage(
mode,
undefined,
serviceTier,
contextItems
contextItems,
);
return;
} catch (error) {
console.error("后端服务不可用:", error);
console.error("处理用户消息失败:", error);
panel.webview.postMessage({
command: "updateStatus",
text: "后端服务不可用",
text: "处理用户消息失败,请稍后重试",
type: "error",
});
// 恢复输入状态
@ -250,7 +238,7 @@ async function handleUserMessageWithBackend(
mode?: RunMode,
reuseTaskId?: string, // 可选,复用现有 taskId用于 Plan 模式确认后继续执行)
serviceTier?: ServiceTier, // 服务等级参数
contextItems?: Array<{ id: number; type: string; path: string }> // 上下文项参数
contextItems?: Array<{ id: number; type: string; path: string }>, // 上下文项参数
): Promise<void> {
const historyManager = ChatHistoryManager.getInstance();
@ -258,7 +246,7 @@ async function handleUserMessageWithBackend(
let enhancedText = text;
if (contextItems && contextItems.length > 0) {
console.log("[MessageHandler] 处理上下文项:", contextItems.length);
const paths = contextItems.map(item => item.path).join('\n');
const paths = contextItems.map((item) => item.path).join("\n");
enhancedText = `${paths}\n\n${text}`;
}
@ -269,15 +257,17 @@ async function handleUserMessageWithBackend(
// 创建会话dialogManager 会自动处理旧会话的中止)
currentSession = dialogManager.createSession(
extensionPath,
taskIdToUse || undefined
taskIdToUse || undefined,
);
// 保存 taskId 用于后续操作(如压缩)
lastTaskId = currentSession.getTaskId();
// 重置离线模式仿真标志(新会话开始)
offlineSimulationTriggered = false;
console.log(
"[MessageHandler] 创建会话: taskId=",
lastTaskId,
"来源=",
taskIdToUse ? "historyManager" : "新生成"
taskIdToUse ? "historyManager" : "新生成",
);
// 显示状态栏
@ -296,11 +286,58 @@ async function handleUserMessageWithBackend(
},
onSegmentUpdate: (segments) => {
// 过滤掉包含 [调用工具:xxx] 的段落
const filteredSegments = segments.filter(seg => {
if (seg.type === 'text' && typeof seg.content === 'string') {
return !/\[调用工具:.+?\]/.test(seg.content);
}
return true;
});
// 实时发送段落更新,按后端返回顺序展示
panel.webview.postMessage({
command: "updateSegments",
segments: segments,
segments: filteredSegments,
});
// 【离线部署模式】检测代码生成完成消息,模拟仿真流程
if (!offlineSimulationTriggered) {
const hasCompletionMessage = segments.some(seg =>
seg.type === 'text' &&
seg.content?.includes('【代码生成完成】') &&
seg.content?.includes('语法检查:已通过')
);
if (hasCompletionMessage) {
offlineSimulationTriggered = true;
console.log('[离线模式] 检测到代码生成完成,开始模拟仿真流程');
// 立即点亮 Simulation 阶段
panel.webview.postMessage({
type: "updateProgress",
step: "simulation"
});
// 随机延时 8-13 秒后完成仿真
const simulationDelay = 8000 + Math.random() * 5000;
setTimeout(() => {
console.log('[离线模式] 模拟仿真完成,进入 Done 阶段');
// Simulation 完成,进入 Done
panel.webview.postMessage({
type: "updateProgress",
step: "done"
});
// 再延时 1 秒完成所有步骤
setTimeout(() => {
console.log('[离线模式] 所有阶段完成');
panel.webview.postMessage({
type: "completeProgress"
});
}, 1000);
}, simulationDelay);
}
}
},
onToolStart: (toolName) => {
@ -320,7 +357,10 @@ async function handleUserMessageWithBackend(
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
},
onQuestion: (askId, question, options) => {
onQuestion: (
askId: string,
questions: import("../types/api").QuestionItem[],
) => {
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
panel.webview.postMessage({
command: "updateStatus",
@ -347,17 +387,6 @@ async function handleUserMessageWithBackend(
console.error("[MessageHandler] 保存AI响应历史失败:", error);
}
// 对话完成后重新获取余额(因为已经消耗了 Credits
try {
console.log("[MessageHandler] 对话完成,重新获取余额...");
const newBalance = await fetchBalance();
if (newBalance !== null) {
console.log("[MessageHandler] 余额已更新:", newBalance);
}
} catch (error) {
console.error("[MessageHandler] 获取余额失败:", error);
}
// 尝试更新面板(如果面板已关闭,这些操作会失败,但不影响数据保存)
try {
// 隐藏状态栏
@ -365,29 +394,36 @@ async function handleUserMessageWithBackend(
command: "hideStatus",
});
// 最后一次发送完整的段落
const result = await panel.webview.postMessage({
// 发送完成标记(不再重复发送 segments避免内容重复显示
panel.webview.postMessage({
command: "updateSegments",
segments: segments,
segments: [],
isComplete: true,
});
console.log("[MessageHandler] postMessage 返回值:", result);
// 发送任务完成消息
panel.webview.postMessage({
command: "taskComplete",
});
// 发送系统通知 - AI 响应完成
const notificationService = NotificationService.getInstance();
notificationService.success(
'IC Coder - AI 响应完成',
'您的问题已得到回复,点击查看详情',
"IC Coder - AI 响应完成",
"您的问题已得到回复,点击查看详情",
() => {
// 点击通知时聚焦到面板
panel.reveal();
}
},
);
// 发送代码变更到前端
sendChangesToWebview(panel);
} catch (error) {
console.warn("[MessageHandler] 更新面板失败(面板可能已关闭):", error);
console.warn(
"[MessageHandler] 更新面板失败(面板可能已关闭):",
error,
);
}
resolve();
@ -399,7 +435,7 @@ async function handleUserMessageWithBackend(
});
panel.webview.postMessage({
command: "receiveMessage",
text: `错误: ${message}`,
text: `错误: ${message}`,
});
// 恢复输入状态
panel.webview.postMessage({
@ -455,7 +491,7 @@ async function handleUserMessageWithBackend(
},
},
mode,
serviceTier // 传递服务等级
serviceTier, // 传递服务等级
);
});
}
@ -466,10 +502,11 @@ async function handleUserMessageWithBackend(
export async function handleUserAnswer(
askId: string,
selected?: string[],
customInput?: string
customInput?: string,
answers?: { [questionIndex: string]: string[] },
): Promise<void> {
if (currentSession) {
await currentSession.submitAnswer(askId, selected, customInput);
await currentSession.submitAnswer(askId, selected, customInput, answers);
}
}
@ -536,7 +573,7 @@ export async function handlePlanAction(
action: string,
planTitle: string,
extensionPath: string,
serviceTier?: ServiceTier
serviceTier?: ServiceTier,
): Promise<void> {
console.log(
"[handlePlanAction] action:",
@ -544,7 +581,7 @@ export async function handlePlanAction(
"planTitle:",
planTitle,
"serviceTier:",
serviceTier
serviceTier,
);
switch (action) {
@ -560,7 +597,7 @@ export async function handlePlanAction(
`请按照刚才的计划执行:${planTitle}`,
extensionPath,
"agent",
serviceTier
serviceTier,
);
break;
@ -577,7 +614,7 @@ export async function handlePlanAction(
`请根据以下建议修改计划:${modification}`,
extensionPath,
"plan",
serviceTier
serviceTier,
);
}
break;
@ -632,7 +669,7 @@ function parseFileOperation(text: string): {
// 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts优先匹配避免被修改匹配
const renameMatch = lowerText.match(
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/,
);
if (renameMatch) {
const oldPath = renameMatch[1].trim();
@ -649,7 +686,7 @@ function parseFileOperation(text: string): {
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
// 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb'
const replaceMatch1 = lowerText.match(
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/,
);
if (replaceMatch1) {
const filePath = replaceMatch1[1].trim();
@ -665,7 +702,7 @@ function parseFileOperation(text: string): {
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
const replaceMatch2 = lowerText.match(
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/,
);
if (replaceMatch2) {
const filePath = replaceMatch2[1].trim();
@ -714,7 +751,7 @@ async function handleFileOperation(
newPath?: string;
searchText?: string;
replaceText?: string;
}
},
) {
const historyManager = ChatHistoryManager.getInstance();
@ -730,7 +767,7 @@ async function handleFileOperation(
text: responseText,
});
vscode.window.showInformationMessage(
`文件创建成功: ${operation.filePath}`
`文件创建成功: ${operation.filePath}`,
);
await historyManager.addAiMessage(responseText);
break;
@ -743,7 +780,7 @@ async function handleFileOperation(
text: responseText,
});
vscode.window.showInformationMessage(
`文件删除成功: ${operation.filePath}`
`文件删除成功: ${operation.filePath}`,
);
await historyManager.addAiMessage(responseText);
break;
@ -779,7 +816,7 @@ async function handleFileOperation(
text: responseText,
});
vscode.window.showInformationMessage(
`文件重命名成功: ${operation.filePath}${operation.newPath}`
`文件重命名成功: ${operation.filePath}${operation.newPath}`,
);
await historyManager.addAiMessage(responseText);
break;
@ -788,21 +825,29 @@ async function handleFileOperation(
if (!operation.searchText || !operation.replaceText) {
throw new Error("缺少替换内容");
}
const oldContentBeforeReplace = await readFileContent(operation.filePath);
const oldContentBeforeReplace = await readFileContent(
operation.filePath,
);
await replaceFile(
operation.filePath,
operation.searchText,
operation.replaceText
operation.replaceText,
);
const newContentAfterReplace = await readFileContent(
operation.filePath,
);
await trackFileChange(
operation.filePath,
oldContentBeforeReplace,
newContentAfterReplace,
);
const newContentAfterReplace = await readFileContent(operation.filePath);
await trackFileChange(operation.filePath, oldContentBeforeReplace, newContentAfterReplace);
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
panel.webview.postMessage({
command: "receiveMessage",
text: responseText,
});
vscode.window.showInformationMessage(
`文件内容替换成功: ${operation.filePath}`
`文件内容替换成功: ${operation.filePath}`,
);
await historyManager.addAiMessage(responseText);
break;
@ -862,7 +907,7 @@ function getDefaultContent(filePath: string): string {
*/
export async function handleReadFile(
panel: vscode.WebviewPanel,
filePath: string
filePath: string,
) {
try {
const content = await readFileContent(filePath);
@ -886,7 +931,7 @@ export async function handleCreateFile(
panel: vscode.WebviewPanel,
filePath: string,
content: string,
overwrite: boolean = false //是否覆盖
overwrite: boolean = false, //是否覆盖
) {
try {
if (overwrite) {
@ -905,11 +950,14 @@ export async function handleCreateFile(
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.success(
'IC Coder - 文件创建',
"IC Coder - 文件创建",
`文件已创建: ${path.basename(filePath)}`,
() => {
vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath));
}
vscode.commands.executeCommand(
"vscode.open",
vscode.Uri.file(filePath),
);
},
);
} catch (error) {
panel.webview.postMessage({
@ -917,7 +965,7 @@ export async function handleCreateFile(
error: error instanceof Error ? error.message : "创建文件失败",
});
vscode.window.showErrorMessage(
`创建文件失败: ${error instanceof Error ? error.message : "未知错误"}`
`创建文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
);
}
}
@ -928,7 +976,7 @@ export async function handleCreateFile(
export async function handleUpdateFile(
panel: vscode.WebviewPanel,
filePath: string,
content: string
content: string,
) {
try {
const oldContent = await readFileContent(filePath);
@ -944,8 +992,8 @@ export async function handleUpdateFile(
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.info(
'IC Coder - 文件更新',
`文件已更新: ${path.basename(filePath)}`
"IC Coder - 文件更新",
`文件已更新: ${path.basename(filePath)}`,
);
} catch (error) {
panel.webview.postMessage({
@ -953,7 +1001,7 @@ export async function handleUpdateFile(
error: error instanceof Error ? error.message : "更新文件失败",
});
vscode.window.showErrorMessage(
`更新文件失败: ${error instanceof Error ? error.message : "未知错误"}`
`更新文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
);
}
}
@ -964,7 +1012,7 @@ export async function handleUpdateFile(
export async function handleRenameFile(
panel: vscode.WebviewPanel,
oldPath: string,
newPath: string
newPath: string,
) {
try {
await renameFile(oldPath, newPath);
@ -975,7 +1023,7 @@ export async function handleRenameFile(
message: "文件重命名成功",
});
vscode.window.showInformationMessage(
`文件重命名成功: ${oldPath}${newPath}`
`文件重命名成功: ${oldPath}${newPath}`,
);
} catch (error) {
panel.webview.postMessage({
@ -983,7 +1031,7 @@ export async function handleRenameFile(
error: error instanceof Error ? error.message : "重命名文件失败",
});
vscode.window.showErrorMessage(
`重命名文件失败: ${error instanceof Error ? error.message : "未知错误"}`
`重命名文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
);
}
}
@ -995,7 +1043,7 @@ export async function handleReplaceInFile(
panel: vscode.WebviewPanel,
filePath: string,
searchText: string,
replaceText: string
replaceText: string,
) {
try {
const oldContent = await readFileContent(filePath);
@ -1014,7 +1062,7 @@ export async function handleReplaceInFile(
error: error instanceof Error ? error.message : "替换文件内容失败",
});
vscode.window.showErrorMessage(
`替换文件内容失败: ${error instanceof Error ? error.message : "未知错误"}`
`替换文件内容失败: ${error instanceof Error ? error.message : "未知错误"}`,
);
}
}
@ -1059,7 +1107,7 @@ function isVCDGenerationCommand(text: string): boolean {
*/
async function handleVCDGeneration(
panel: vscode.WebviewPanel,
extensionPath: string
extensionPath: string,
) {
try {
// 获取当前工作区路径
@ -1086,7 +1134,7 @@ async function handleVCDGeneration(
if (!iverilogCheck.available) {
panel.webview.postMessage({
command: "receiveMessage",
text: `${iverilogCheck.message}\n\n请参考插件文档安装 iverilog 工具`,
text: `${iverilogCheck.message}`,
});
vscode.window.showErrorMessage(iverilogCheck.message);
return;
@ -1161,12 +1209,15 @@ async function handleVCDGeneration(
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.success(
'IC Coder - 仿真完成',
"IC Coder - 仿真完成",
`VCD 文件已生成: ${fileName}`,
() => {
// 点击通知时打开 VCD 查看器
vscode.commands.executeCommand('ic-coder.openVCDViewer', result.vcdFilePath);
}
vscode.commands.executeCommand(
"ic-coder.openVCDViewer",
result.vcdFilePath,
);
},
);
} else {
panel.webview.postMessage({
@ -1195,12 +1246,12 @@ async function handleVCDGeneration(
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.error(
'IC Coder - 仿真失败',
'VCD 文件生成失败,请查看错误信息',
"IC Coder - 仿真失败",
"VCD 文件生成失败,请查看错误信息",
() => {
// 点击通知时聚焦到面板
panel.reveal();
}
},
);
}
} catch (error) {
@ -1218,11 +1269,11 @@ async function handleVCDGeneration(
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.error(
'IC Coder - 仿真错误',
error instanceof Error ? error.message : '生成 VCD 文件时出错',
"IC Coder - 仿真错误",
error instanceof Error ? error.message : "生成 VCD 文件时出错",
() => {
panel.reveal();
}
},
);
}
}
@ -1232,7 +1283,7 @@ async function handleVCDGeneration(
*/
export async function handleOptimizePrompt(
panel: vscode.WebviewPanel,
prompt: string
prompt: string,
): Promise<void> {
console.log("[MessageHandler] ========== 收到提示词优化请求 ==========");
console.log("[MessageHandler] prompt:", prompt);
@ -1264,7 +1315,7 @@ export async function handleOptimizePrompt(
*/
export async function handleAcceptChange(
panel: vscode.WebviewPanel,
changeId: string
changeId: string,
) {
try {
const success = await changeTracker.acceptChange(changeId);
@ -1272,14 +1323,14 @@ export async function handleAcceptChange(
panel.webview.postMessage({
command: "changeAccepted",
changeId: changeId,
success: true
success: true,
});
} else {
panel.webview.postMessage({
command: "changeAccepted",
changeId: changeId,
success: false,
error: "采纳变更失败"
error: "采纳变更失败",
});
}
} catch (error) {
@ -1288,7 +1339,7 @@ export async function handleAcceptChange(
command: "changeAccepted",
changeId: changeId,
success: false,
error: String(error)
error: String(error),
});
}
}
@ -1298,7 +1349,7 @@ export async function handleAcceptChange(
*/
export async function handleRejectChange(
panel: vscode.WebviewPanel,
changeId: string
changeId: string,
) {
try {
const success = await changeTracker.rejectChange(changeId);
@ -1306,14 +1357,14 @@ export async function handleRejectChange(
panel.webview.postMessage({
command: "changeRejected",
changeId: changeId,
success: true
success: true,
});
} else {
panel.webview.postMessage({
command: "changeRejected",
changeId: changeId,
success: false,
error: "拒绝变更失败"
error: "拒绝变更失败",
});
}
} catch (error) {
@ -1322,7 +1373,7 @@ export async function handleRejectChange(
command: "changeRejected",
changeId: changeId,
success: false,
error: String(error)
error: String(error),
});
}
}
@ -1333,18 +1384,18 @@ export async function handleRejectChange(
export function sendChangesToWebview(panel: vscode.WebviewPanel) {
const session = changeTracker.endSession();
if (session && session.changes.length > 0) {
const changesWithDiff = session.changes.map(change => {
const changesWithDiff = session.changes.map((change) => {
const diffLines = generateDiff(change.oldContent, change.newContent);
const diffHtml = renderDiffHtml(diffLines);
return {
...change,
diffHtml
diffHtml,
};
});
panel.webview.postMessage({
command: "showChanges",
changes: changesWithDiff
changes: changesWithDiff,
});
}
}
@ -1361,62 +1412,67 @@ export function startChangeSession(sessionId: string) {
*/
export async function handleOpenFileDiff(
panel: vscode.WebviewPanel,
changeId: string
changeId: string,
) {
try {
const session = changeTracker.getCurrentSession();
if (!session) {
vscode.window.showErrorMessage('没有找到变更会话');
vscode.window.showErrorMessage("没有找到变更会话");
return;
}
const change = session.changes.find(c => c.changeId === changeId);
const change = session.changes.find((c) => c.changeId === changeId);
if (!change) {
vscode.window.showErrorMessage('没有找到该变更');
vscode.window.showErrorMessage("没有找到该变更");
return;
}
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
vscode.window.showErrorMessage('没有打开的工作区');
vscode.window.showErrorMessage("没有打开的工作区");
return;
}
// 创建临时文件用于对比
const filePath = change.filePath;
const absolutePath = vscode.Uri.file(
path.join(workspaceFolder.uri.fsPath, filePath)
path.join(workspaceFolder.uri.fsPath, filePath),
);
// 创建虚拟文档显示旧内容
const oldUri = vscode.Uri.parse(
`ic-coder-diff:${filePath}.old?${changeId}`
).with({ scheme: 'ic-coder-diff' });
`ic-coder-diff:${filePath}.old?${changeId}`,
).with({ scheme: "ic-coder-diff" });
// 注册文档内容提供者(如果还没注册)
if (!(global as any).__diffProviderRegistered) {
const provider = new (class implements vscode.TextDocumentContentProvider {
const provider = new (class
implements vscode.TextDocumentContentProvider
{
provideTextDocumentContent(uri: vscode.Uri): string {
const changeId = uri.query;
const session = changeTracker.getCurrentSession();
const change = session?.changes.find(c => c.changeId === changeId);
return change?.oldContent || '';
const change = session?.changes.find((c) => c.changeId === changeId);
return change?.oldContent || "";
}
})();
vscode.workspace.registerTextDocumentContentProvider('ic-coder-diff', provider);
vscode.workspace.registerTextDocumentContentProvider(
"ic-coder-diff",
provider,
);
(global as any).__diffProviderRegistered = true;
}
// 打开 diff 编辑器
await vscode.commands.executeCommand(
'vscode.diff',
"vscode.diff",
oldUri,
absolutePath,
`${filePath} (变更对比)`
`${filePath} (变更对比)`,
);
} catch (error) {
console.error('[MessageHandler] 打开 diff 失败:', error);
console.error("[MessageHandler] 打开 diff 失败:", error);
vscode.window.showErrorMessage(`打开 diff 失败: ${error}`);
}
}

View File

@ -129,6 +129,62 @@ export async function readDirectory(
}
}
/**
* 列出目录下的文件和文件夹(不读取内容,仅返回路径)
*/
export async function listDirectory(
dirPath: string,
extensions?: string[]
): Promise<string[]> {
try {
// 如果是相对路径,转换为绝对路径
let absolutePath = dirPath;
if (!path.isAbsolute(dirPath)) {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, dirPath);
}
}
const dirUri = vscode.Uri.file(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}`);
}
// 读取目录内容
const entries = await vscode.workspace.fs.readDirectory(dirUri);
const results: string[] = [];
for (const [fileName, fileType] of entries) {
if (fileType === vscode.FileType.Directory) {
results.push(fileName + '/');
} else if (fileType === vscode.FileType.File) {
// 扩展名过滤
if (extensions && extensions.length > 0) {
const ext = path.extname(fileName);
// 规范化扩展名(支持 "v" 和 ".v" 两种格式)
const normalizedExts = extensions.map(e => e.startsWith('.') ? e : '.' + e);
if (!normalizedExts.includes(ext)) {
continue;
}
}
results.push(fileName);
}
}
return results;
} catch (error) {
throw error;
}
}
/**
* 获取文件信息
*/

View File

@ -92,7 +92,8 @@ export async function executeWaveformTrace(
child.on('close', (code: number | null) => {
if (code === 0) {
resolve(stdout);
// 成功时返回 stdout忽略 stderr 中的进度信息
resolve(stdout || stderr);
} else {
reject(new Error(
`waveform_trace 执行失败 (code=${code}):\n${stderr || stdout}`

View File

@ -13,6 +13,7 @@ import {
abortCurrentDialog,
handleOptimizePrompt,
} from "../utils/messageHandler";
import { setCustomConfig } from "../config/settings";
/**
* 创建并显示IC 侧边栏视图
@ -28,7 +29,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.joinPath(context.extensionUri, "media"),
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
vscode.Uri.joinPath(context.extensionUri, "dist", "assets")
],
}
);
@ -47,21 +48,21 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
// 获取模型图标URI
const autoIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "Auto.png")
);
const liteIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "lite.png")
);
const syIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "Sy.png")
);
const maxIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "model", "Max.png")
);
// 获取二维码图片URI
const qrCodeUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "QRCode", "wx.png")
vscode.Uri.joinPath(context.extensionUri, "dist", "assets", "QRCode", "wx.png")
);
// 获取Logo URI
@ -124,12 +125,17 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
case "showWarning":
vscode.window.showWarningMessage(message.message);
break;
// 新增:打开用户手册
case "openUserManual":
vscode.commands.executeCommand("ic-coder.openUserManual");
break;
// 新增:处理用户回答
case "submitAnswer":
handleUserAnswer(
message.askId,
message.selected,
message.customInput
message.customInput,
message.answers
);
break;
// 新增:中止对话
@ -140,6 +146,21 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
case "optimizePrompt":
handleOptimizePrompt(panel, message.prompt);
break;
// 保存通用设置
case "saveGeneralSettings":
context.globalState.update('generalSettings', message.settings);
// 更新运行时配置(包括清空)
setCustomConfig({ backendUrl: message.settings.backendUrl || '' });
vscode.window.showInformationMessage('设置已保存');
break;
// 加载通用设置
case "loadGeneralSettings":
const settings = context.globalState.get('generalSettings');
panel.webview.postMessage({
command: 'loadedGeneralSettings',
settings: settings
});
break;
}
},
undefined,
@ -157,52 +178,21 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
private readonly extensionUri: vscode.Uri,
private readonly context: vscode.ExtensionContext
) {
// 监听认证状态变化
this.context.subscriptions.push(
vscode.authentication.onDidChangeSessions((e) => {
if (e.provider.id === "iccoder") {
this.refreshLoginStatus();
}
})
);
// 【已禁用】监听认证状态变化 - 无需登录
}
/**
* 刷新登录状态并更新视图
* 【已禁用】刷新登录状态并更新视图 - 无需登录
*/
private async refreshLoginStatus(): Promise<void> {
if (this._view) {
const isLoggedIn = await this.checkLoginStatus();
this._view.webview.html = this.getWebviewContent(
this._view.webview,
isLoggedIn
);
}
// 无需刷新登录状态
}
/**
* 检查登录状态(使用 Authentication API
* 【已禁用】检查登录状态 - 无需登录
*/
private async checkLoginStatus(): Promise<boolean> {
try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
console.log("[ICViewProvider] 检查登录状态, session:", session ? "存在" : "不存在");
if (!session) {
return false;
}
// 检查 token 是否过期
const expired = isTokenExpired(session.accessToken);
console.log("[ICViewProvider] token 过期检查结果:", expired);
// 只有明确过期才认为未登录,无法判断时认为已登录
if (expired === true) {
console.log("[ICViewProvider] Token 已过期");
return false;
}
return true;
} catch (error) {
console.log("[ICViewProvider] 检查登录状态失败:", error);
return false;
}
return true; // 始终返回已登录状态
}
resolveWebviewView(webviewView: vscode.WebviewView) {
@ -222,30 +212,8 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
console.log('[ICViewProvider] Webview options 已设置');
console.log('[ICViewProvider] extensionUri:', this.extensionUri.toString());
// 【关键修复】先设置默认 HTML避免一直加载
try {
const html = this.getWebviewContent(webviewView.webview, false);
console.log('[ICViewProvider] HTML 内容已生成,长度:', html.length);
webviewView.webview.html = html;
console.log('[ICViewProvider] HTML 已设置到 webview');
} catch (error) {
console.error('[ICViewProvider] 设置 HTML 失败:', error);
}
// 异步检查登录状态并更新 UI
this.checkLoginStatus()
.then((isLoggedIn) => {
console.log('[ICViewProvider] 登录状态检查完成:', isLoggedIn);
webviewView.webview.html = this.getWebviewContent(
webviewView.webview,
isLoggedIn
);
})
.catch((error) => {
console.error('[ICViewProvider] 检查登录状态失败:', error);
// 即使失败也显示未登录状态
webviewView.webview.html = this.getWebviewContent(webviewView.webview, false);
});
// 【已禁用】登录检查 - 直接显示"开始使用"按钮
webviewView.webview.html = this.getWebviewContent(webviewView.webview, true);
// 处理侧边栏的消息
webviewView.webview.onDidReceiveMessage(
@ -256,15 +224,32 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
vscode.commands.executeCommand("ic-coder.login");
} else if (message.command === "logout") {
// 退出登录(前端已有确认对话框)
vscode.commands.executeCommand('iccoder.logout');
vscode.commands.executeCommand("ic-coder.logout");
} else if (message.command === "openICCoder") {
// 打开 IC Coder 官网
vscode.env.openExternal(vscode.Uri.parse('https://www.iccoder.com'));
} else if (message.command === "openUserManual") {
// 打开用户手册
vscode.commands.executeCommand("ic-coder.openUserManual");
} else if (message.command === "openExternalUrl") {
// 打开外部链接
if (message.url) {
vscode.env.openExternal(vscode.Uri.parse(message.url));
}
} else if (message.command === "saveGeneralSettings") {
// 保存通用设置
this.context.globalState.update('generalSettings', message.settings);
if (message.settings.backendUrl) {
setCustomConfig({ backendUrl: message.settings.backendUrl });
}
vscode.window.showInformationMessage('设置已保存');
} else if (message.command === "loadGeneralSettings") {
// 加载通用设置
const settings = this.context.globalState.get('generalSettings');
webviewView.webview.postMessage({
command: 'loadedGeneralSettings',
settings: settings
});
}
},
undefined,
@ -320,15 +305,15 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
width: 200px;
padding: 8px 12px;
margin: 4px 0;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
background: #007ACC;
color: #ffffff;
border: none;
border-radius: 4px;
cursor: pointer;
text-align: center;
}
.btn:hover {
background: var(--vscode-button-hoverBackground);
background: #005a9e;
}
</style>
</head>
@ -337,7 +322,7 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
<img src="${logoUri}" alt="IC Coder" width="120" />
<h2>欢迎使用 IC Coder</h2>
${isLoggedIn
? '<button class="btn" onclick="openChat()">开始创作</button>'
? '<button class="btn" onclick="openChat()">开始使用</button>'
: '<button class="btn" onclick="login()">登录账户</button>'
}
</div>

View File

@ -16,7 +16,7 @@ export function getContextButtonContent(): string {
<span class="add-context-label">添加上下文</span>
</button>
<span class="tooltiptext">添加文件、文件夹、图片或文档作为上下文</span>
<span class="tooltiptext">添加文件、文件夹作为上下文</span>
</div>
<!-- 上拉菜单 -->
@ -271,6 +271,7 @@ export function getContextButtonStyles(): string {
width: 14px;
height: 14px;
flex-shrink: 0;
pointer-events: none;
}
.context-menu-list-item label {
@ -335,6 +336,7 @@ export function getContextButtonScript(): string {
return `
// 上下文菜单状态
let currentListData = [];
let filteredListData = [];
let currentListType = '';
let selectedItems = new Set();
@ -392,6 +394,15 @@ export function getContextButtonScript(): string {
selectedItems.clear();
currentListData = [];
filteredListData = [];
clearContextSearchInput();
}
function clearContextSearchInput() {
const searchInput = document.getElementById('contextMenuSearch');
if (searchInput) {
searchInput.value = '';
}
}
// 切换到列表视图
@ -406,10 +417,12 @@ export function getContextButtonScript(): string {
titleEl.textContent = title;
currentListType = type;
currentListData = data;
currentListData = data || [];
filteredListData = currentListData;
selectedItems.clear();
renderList(data);
clearContextSearchInput();
renderList(filteredListData);
updateSelectedCount();
}
}
@ -419,32 +432,36 @@ export function getContextButtonScript(): string {
const body = document.getElementById('contextMenuListBody');
if (!body) return;
body.innerHTML = data.map((item, index) => \`
<div class="context-menu-list-item" onclick="toggleItemSelection(\${index})">
<input type="checkbox" id="item-\${index}" />
<label for="item-\${index}">\${item.relativePath}</label>
filteredListData = data || [];
body.innerHTML = filteredListData.map((item, index) => \`
<div class="context-menu-list-item \${selectedItems.has(item.path) ? 'selected' : ''}" onclick="toggleItemSelection(\${index})">
<input type="checkbox" id="item-\${index}" \${selectedItems.has(item.path) ? 'checked' : ''} />
<label>\${item.relativePath || item.path}</label>
</div>
\`).join('');
}
// 切换项选择
function toggleItemSelection(index) {
const selectedItem = filteredListData[index];
if (!selectedItem) return;
const selectedPath = selectedItem.path;
const checkbox = document.getElementById('item-' + index);
const item = document.querySelectorAll('.context-menu-list-item')[index];
if (checkbox && item) {
checkbox.checked = !checkbox.checked;
if (checkbox.checked) {
selectedItems.add(index);
item.classList.add('selected');
} else {
selectedItems.delete(index);
item.classList.remove('selected');
}
updateSelectedCount();
if (selectedItems.has(selectedPath)) {
selectedItems.delete(selectedPath);
if (checkbox) checkbox.checked = false;
if (item) item.classList.remove('selected');
} else {
selectedItems.add(selectedPath);
if (checkbox) checkbox.checked = true;
if (item) item.classList.add('selected');
}
updateSelectedCount();
}
// 更新选中数量
@ -457,15 +474,25 @@ export function getContextButtonScript(): string {
// 确认选择
function confirmSelection() {
const selected = Array.from(selectedItems).map(index => currentListData[index]);
try {
const selected = currentListData.filter(item => selectedItems.has(item.path));
if (selected.length > 0) {
selected.forEach(item => {
addContextItem(currentListType, item.path);
});
if (selected.length > 0) {
selected.forEach(item => {
addContextItem(currentListType, item.path, item.relativePath || item.path);
});
}
} finally {
const menu = document.getElementById('contextMenu');
const button = document.querySelector('.add-context-button');
if (menu) {
menu.classList.remove('show');
}
if (button) {
button.classList.remove('active');
}
backToMainMenu();
}
toggleContextMenu();
}
// 添加图片
@ -484,9 +511,9 @@ export function getContextButtonScript(): string {
const searchInput = document.getElementById('contextMenuSearch');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
const keyword = e.target.value.toLowerCase();
const keyword = (e.target.value || '').toLowerCase().trim();
const filtered = currentListData.filter(item =>
item.relativePath.toLowerCase().includes(keyword)
(item.relativePath || item.path || '').toLowerCase().includes(keyword)
);
renderList(filtered);
});

View File

@ -51,7 +51,11 @@ export function getContextDisplayStyles(): string {
transition: all 0.2s ease;
}
.context-item:hover {
.context-item.clickable {
cursor: pointer;
}
.context-item.clickable:hover {
background: var(--vscode-list-hoverBackground);
}
@ -126,6 +130,11 @@ export function getContextDisplayScript(): string {
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
}
// 获取代码图标 SVG
function getCodeIcon() {
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z m-484.7-122.1l39.6-39.5 113.1 113.1-39.6 39.5-113.1-113.1z m226.4-290.2l113.1 113.1-39.6 39.5-113.1-113.1 39.6-39.5z" fill="currentColor"/></svg>';
}
// 获取删除图标 SVG
function getRemoveIcon() {
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9c-4.4 5.2-.7 13.1 6.1 13.1h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" fill="currentColor"/></svg>';
@ -137,9 +146,12 @@ export function getContextDisplayScript(): string {
}
// 添加上下文项
function addContextItem(type, path) {
function addContextItem(type, path, displayPath) {
const exists = contextItems.some(item => item.type === type && item.path === path);
if (exists) return;
const id = Date.now() + Math.random();
contextItems.push({ id, type, path });
contextItems.push({ id, type, path, displayPath: displayPath || '' });
renderContextItems();
}
@ -169,13 +181,17 @@ export function getContextDisplayScript(): string {
case 'folder': icon = getFolderIcon(); break;
case 'image': icon = getImageIcon(); break;
case 'document': icon = getDocumentIcon(); break;
case 'code': icon = getCodeIcon(); break;
}
const clickable = item.type !== 'folder' ? 'clickable' : '';
const onclick = item.type !== 'folder' ? \`onclick="window.handleContextItemClick(\${item.id})"\` : '';
return \`
<div class="context-item" title="\${item.path}">
<div class="context-item \${clickable}" title="\${item.path || item.displayPath}" \${onclick}>
\${icon}
<span class="context-item-name">\${getFileName(item.path)}</span>
<span class="context-item-remove" onclick="removeContextItem(\${item.id})">
<span class="context-item-name">\${item.displayPath || getFileName(item.path)}</span>
<span class="context-item-remove" onclick="event.stopPropagation(); removeContextItem(\${item.id})">
\${getRemoveIcon()}
</span>
</div>
@ -183,6 +199,27 @@ export function getContextDisplayScript(): string {
}).join('');
}
// 全局访问函数
window.handleContextItemClick = function(id) {
const item = contextItems.find(i => i.id === id);
if (!item || item.type === 'folder') return;
if (item.type === 'code') {
const codeData = JSON.parse(item.path);
vscode.postMessage({
command: 'openFileWithSelection',
filePath: codeData.fileName,
startLine: codeData.startLine,
endLine: codeData.endLine
});
} else {
vscode.postMessage({
command: 'openFile',
filePath: item.path
});
}
};
// 处理后端返回的文件选择结果
window.addEventListener('message', event => {
const message = event.data;
@ -208,6 +245,18 @@ export function getContextDisplayScript(): string {
message.documents.forEach(doc => addContextItem('document', doc));
}
break;
case 'addCodeContext':
// 添加代码上下文
const displayName = \`\${message.fileName.split(/[\\\\/]/).pop()}:\${message.startLine}-\${message.endLine}\`;
const codeData = {
fileName: message.fileName,
startLine: message.startLine,
endLine: message.endLine,
code: message.code,
languageId: message.languageId
};
addContextItem('code', JSON.stringify(codeData), displayName);
break;
}
});

View File

@ -47,11 +47,7 @@ export function getConversationHistoryBarContent(): string {
</svg>
</button>
<div class="user-info-container">
<button class="user-avatar-icon-button" id="userAvatarIconButton" style="display: none;" title="查看用户信息" onclick="openUserDetailModal()">
${userAvatarIconSvg}
</button>
${getUserInfoComponentContent()}
<div class="user-info-container" style="display: none;">
</div>
<div class='setting'>
@ -330,7 +326,7 @@ export function getConversationHistoryBarStyles(): string {
}
.new-conversation-button:hover {
background: var(--vscode-toolbar-hoverBackground);
background: #007ACC;
transform: scale(1.1);
}

View File

@ -4,7 +4,14 @@
export function getExampleShowcaseContent(): string {
return `
<div class="example-showcase" id="exampleShowcase">
<div class="showcase-title">示例</div>
<div class="showcase-header">
<div class="showcase-title">示例</div>
<button class="refresh-button" onclick="refreshExamples()" title="换一批">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.5 2V8M21.5 8H15.5M21.5 8L18 4.5C16.7429 3.24286 15.1767 2.35596 13.4606 1.93597C11.7446 1.51598 9.94736 1.57986 8.26381 2.12059C6.58027 2.66131 5.07831 3.65985 3.91872 4.99987C2.75913 6.33989 1.98648 7.96902 1.68 9.71M2.5 22V16M2.5 16H8.5M2.5 16L6 19.5C7.25714 20.7571 8.82331 21.644 10.5394 22.064C12.2554 22.484 14.0526 22.4201 15.7362 21.8794C17.4197 21.3387 18.9217 20.3401 20.0813 19.0001C21.2409 17.6601 22.0135 16.031 22.32 14.29" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<div class="example-cards">
<div class="example-card" onclick="sendExample(0)">
<div class="example-icon">
@ -34,14 +41,6 @@ export function getExampleShowcaseContent(): string {
</div>
</div>
</div>
<div class="web-link">
<a href="https://iccoder.com" target="_blank" class="web-link-button">
<span class="link-icon">🌐</span>
<span>IC Coder Web端</span>
<span class="link-arrow">→</span>
</a>
</div>
</div>
`;
}
@ -62,12 +61,44 @@ export function getExampleShowcaseStyles(): string {
display: none;
}
.showcase-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.showcase-title {
font-size: 14px;
font-weight: 600;
color: var(--vscode-foreground);
margin-bottom: 12px;
text-align: left;
}
.refresh-button {
background: transparent;
border: none;
color: var(--vscode-foreground);
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
opacity: 0.6;
}
.refresh-button:hover {
opacity: 1;
background: var(--vscode-input-background);
}
.refresh-button svg {
transition: transform 0.3s ease;
}
.refresh-button:active svg {
transform: rotate(180deg);
}
.example-cards {
@ -165,41 +196,6 @@ export function getExampleShowcaseStyles(): string {
-webkit-box-orient: vertical;
}
.web-link {
display: flex;
justify-content: center;
padding-top: 20px;
border-top: 1px solid var(--vscode-panel-border);
margin-top: 8px;
}
.web-link-button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: transparent;
border: none;
text-decoration: none;
font-size: 14px;
font-weight: 600;
transition: all 0.2s ease;
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 50%, #a855f7 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
outline: none;
}
.web-link-button:focus {
outline: none;
}
.web-link-button:hover {
transform: translateY(-1px);
opacity: 0.8;
}
.link-icon {
font-size: 16px;
}
@ -208,10 +204,6 @@ export function getExampleShowcaseStyles(): string {
font-size: 16px;
transition: transform 0.2s ease;
}
.web-link-button:hover .link-arrow {
transform: translateX(3px);
}
`;
}
@ -220,15 +212,74 @@ export function getExampleShowcaseStyles(): string {
*/
export function getExampleShowcaseScript(): string {
return `
// 示例文本数组
const exampleTexts = [
'生成一个SPI控制器',
'生成一个GMII接口的以太网UDP通信模块'
// 所有可用的示例
const allExamples = [
'设计一个算术逻辑单元,完成常见运算',
'实现一个优先编码器,多个输入同时有效时,只输出优先级最高的那个编号',
'实现一个译码器,把二进制编号转换成 one-hot 输出',
'实现一个移位寄存器,完成串行/并行数据移位与装载',
'实现一个按键消抖模块,解决机械按键抖动问题',
'实现一个跑马灯控制器,控制 LED 形成不同流动效果',
'实现一个序列检测器,检测串行输入中是否出现指定比特序列',
'实现一个LFSR 伪随机数发生器',
'实现一个自动售货机,模拟一个简单售货逻辑',
'实现一个交通灯控制器,控制两方向交通灯的切换',
'实现一个先进先出的数据缓冲区',
'单端口 RAM 读写控制器',
'实现一个移位加法乘法器,不用 * 运算符'
];
// 当前显示的示例文本
let exampleTexts = ['生成一个SPI控制器', '生成一个GMII接口的以太网UDP通信模块'];
// 存储待发送的示例索引
let pendingExampleIndex = -1;
// 节流控制
let refreshing = false;
// 刷新示例
function refreshExamples() {
if (refreshing) return;
refreshing = true;
const used = new Set();
const newExamples = [];
while (newExamples.length < 2) {
const idx = Math.floor(Math.random() * allExamples.length);
if (!used.has(idx)) {
used.add(idx);
newExamples.push(allExamples[idx]);
}
}
exampleTexts = newExamples;
updateExampleCards();
setTimeout(() => { refreshing = false; }, 500);
}
// 更新示例卡片显示
function updateExampleCards() {
const container = document.querySelector('.example-cards');
if (!container) return;
container.innerHTML = exampleTexts.map((text, i) => \`
<div class="example-card" onclick="sendExample(\${i})">
<div class="example-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2H6C5.46957 2 4.96086 2.21071 4.58579 2.58579C4.21071 2.96086 4 3.46957 4 4V20C4 20.5304 4.21071 21.0391 4.58579 21.4142C4.96086 21.7893 5.46957 22 6 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 13H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 17H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 9H9H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="example-content">
<div class="example-title">\${text}</div>
</div>
</div>
\`).join('');
}
// 直接发送示例消息
function sendExample(index) {
// 先检查邀请码验证状态

73
src/views/filePathTag.ts Normal file
View File

@ -0,0 +1,73 @@
/**
* 文件路径标签组件
* 功能:显示可点击的文件路径标签
* 使用场景:在用户消息中显示上下文文件
*/
/**
* 获取文件路径标签的样式
*/
export function getFilePathTagStyles(): string {
return `
/* 文件路径标签 */
.file-path-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
margin-right: 6px;
background: rgba(0, 122, 204, 0.15);
border: 1px solid rgba(0, 122, 204, 0.3);
border-radius: 4px;
color: #4fc3f7;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
font-family: 'Consolas', 'Monaco', monospace;
}
.file-path-tag:hover {
background: rgba(0, 122, 204, 0.25);
border-color: rgba(0, 122, 204, 0.5);
}
.file-path-tag svg {
width: 12px;
height: 12px;
opacity: 0.8;
}
`;
}
/**
* 获取文件路径标签的脚本
*/
export function getFilePathTagScript(): string {
return `
// 处理文件路径标签点击
function handleFilePathClick(filePath) {
// 解析文件路径,支持 file.v:5-8 格式
const match = filePath.match(/^(.+?):(\\d+)-(\\d+)$/);
if (match) {
vscode.postMessage({
command: 'openFilePathTag',
filePath: match[1],
startLine: parseInt(match[2]),
endLine: parseInt(match[3])
});
} else {
vscode.postMessage({
command: 'openFilePathTag',
filePath: filePath
});
}
}
// 创建文件路径标签
window.createFilePathTag = function(filePath) {
const fileIcon = '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7z" fill="currentColor"/></svg>';
const escapedPath = filePath.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
return '<span class="file-path-tag" onclick="handleFilePathClick(\\'' + escapedPath + '\\')">' + fileIcon + filePath + '</span>';
};
`;
}

View File

@ -4,75 +4,15 @@
export function getGeneralSettingsComponentContent(): string {
return `
<div class="general-settings">
<h3 class="settings-section-title">通用设置</h3>
<h3 class="settings-section-title">后端服务配置</h3>
<div class="settings-section">
<div class="settings-item">
<div class="settings-item-header">
<label class="settings-item-label">主题</label>
<span class="settings-item-description">选择界面主题</span>
<label class="settings-item-label">后端服务地址</label>
<span class="settings-item-description">自定义后端 API 地址</span>
</div>
<select class="settings-select" id="themeSelect">
<option value="auto">跟随系统</option>
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
</div>
<div class="settings-item">
<div class="settings-item-header">
<label class="settings-item-label">语言</label>
<span class="settings-item-description">选择界面语言</span>
</div>
<select class="settings-select" id="languageSelect">
<option value="zh-CN">简体中文</option>
<option value="en-US">English</option>
</select>
</div>
<div class="settings-item">
<div class="settings-item-header">
<label class="settings-item-label">自动保存</label>
<span class="settings-item-description">自动保存会话历史</span>
</div>
<label class="settings-switch">
<input type="checkbox" id="autoSaveCheckbox" checked>
<span class="settings-switch-slider"></span>
</label>
</div>
<div class="settings-item">
<div class="settings-item-header">
<label class="settings-item-label">显示时间戳</label>
<span class="settings-item-description">在消息中显示时间戳</span>
</div>
<label class="settings-switch">
<input type="checkbox" id="showTimestampCheckbox">
<span class="settings-switch-slider"></span>
</label>
</div>
</div>
<div class="settings-section">
<h4 class="settings-subsection-title">编辑器设置</h4>
<div class="settings-item">
<div class="settings-item-header">
<label class="settings-item-label">字体大小</label>
<span class="settings-item-description">设置编辑器字体大小</span>
</div>
<input type="number" class="settings-input" id="fontSizeInput" value="14" min="10" max="24">
</div>
<div class="settings-item">
<div class="settings-item-header">
<label class="settings-item-label">代码高亮</label>
<span class="settings-item-description">启用代码语法高亮</span>
</div>
<label class="settings-switch">
<input type="checkbox" id="syntaxHighlightCheckbox" checked>
<span class="settings-switch-slider"></span>
</label>
<input type="text" class="settings-input-text" id="backendUrlInput" placeholder="https://iccoder.com">
</div>
</div>
@ -176,6 +116,21 @@ export function getGeneralSettingsComponentStyles(): string {
border-color: var(--vscode-focusBorder);
}
.settings-input-text {
width: 300px;
padding: 6px 12px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
font-size: 13px;
outline: none;
}
.settings-input-text:focus {
border-color: var(--vscode-focusBorder);
}
.settings-switch {
position: relative;
display: inline-block;
@ -270,57 +225,37 @@ export function getGeneralSettingsComponentScript(): string {
// 保存通用设置
function saveGeneralSettings() {
const settings = {
theme: document.getElementById('themeSelect').value,
language: document.getElementById('languageSelect').value,
autoSave: document.getElementById('autoSaveCheckbox').checked,
showTimestamp: document.getElementById('showTimestampCheckbox').checked,
fontSize: document.getElementById('fontSizeInput').value,
syntaxHighlight: document.getElementById('syntaxHighlightCheckbox').checked,
backendUrl: document.getElementById('backendUrlInput').value,
};
// 发送消息到扩展
vscode.postMessage({
command: 'saveGeneralSettings',
settings: settings
});
// 显示保存成功提示
console.log('通用设置已保存', settings);
closeSettingsModal();
}
// 重置通用设置
function resetGeneralSettings() {
document.getElementById('themeSelect').value = 'auto';
document.getElementById('languageSelect').value = 'zh-CN';
document.getElementById('autoSaveCheckbox').checked = true;
document.getElementById('showTimestampCheckbox').checked = false;
document.getElementById('fontSizeInput').value = '14';
document.getElementById('syntaxHighlightCheckbox').checked = true;
document.getElementById('backendUrlInput').value = '';
// 清空保存的配置
vscode.postMessage({
command: 'saveGeneralSettings',
settings: { backendUrl: '' }
});
console.log('通用设置已重置为默认值');
closeSettingsModal();
}
// 加载通用设置
function loadGeneralSettings(settings) {
if (!settings) return;
if (settings.theme) {
document.getElementById('themeSelect').value = settings.theme;
}
if (settings.language) {
document.getElementById('languageSelect').value = settings.language;
}
if (settings.autoSave !== undefined) {
document.getElementById('autoSaveCheckbox').checked = settings.autoSave;
}
if (settings.showTimestamp !== undefined) {
document.getElementById('showTimestampCheckbox').checked = settings.showTimestamp;
}
if (settings.fontSize) {
document.getElementById('fontSizeInput').value = settings.fontSize;
}
if (settings.syntaxHighlight !== undefined) {
document.getElementById('syntaxHighlightCheckbox').checked = settings.syntaxHighlight;
if (settings.backendUrl) {
document.getElementById('backendUrlInput').value = settings.backendUrl;
}
}
`;

View File

@ -24,6 +24,10 @@ import {
getContextCompressStyles,
getContextCompressScript,
} from "./contextCompress";
import {
getFilePathTagStyles,
getFilePathTagScript,
} from "./filePathTag";
import {
getOptimizeButtonContent,
getOptimizeButtonStyles,
@ -98,6 +102,7 @@ export function getInputAreaStyles(): string {
${getModelSelectorStyles()}
${getContextButtonStyles()}
${getContextDisplayStyles()}
${getFilePathTagStyles()}
${getContextCompressStyles()}
${getOptimizeButtonStyles()}
${getExampleShowcaseStyles()}
@ -309,6 +314,7 @@ export function getInputAreaScript(): string {
${getContextCompressScript()}
${getOptimizeButtonScript()}
${getChangePanelScript()}
${getFilePathTagScript()}
// 对话状态管理
let isConversationActive = false;
@ -426,7 +432,21 @@ export function getInputAreaScript(): string {
// 获取上下文项
const contextItems = window.getContextItems ? window.getContextItems() : [];
addMessage(text, 'user');
// 构建显示消息:如果有上下文项,添加路径前缀
let displayText = text;
if (contextItems.length > 0) {
const contextPaths = contextItems
.map(item => item.displayPath || item.path)
.join(' ');
if (contextPaths) {
displayText = contextPaths + ' ' + text;
}
}
addMessage(displayText, 'user');
// 重置分段消息容器,强制下次创建新容器
currentSegmentedMessage = null;
// 标记已有消息,切换布局到底部
hasMessages = true;

View File

@ -248,19 +248,21 @@ export function getMessageAreaStyles(): string {
}
.question-option {
padding: 8px 16px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-button-border);
background: #007ACC;
color: #ffffff;
border: 1px solid #007ACC;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.question-option:hover {
background: var(--vscode-button-secondaryHoverBackground);
background: #005a9e;
border-color: #005a9e;
}
.question-option.selected {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
background: #007ACC;
color: #ffffff;
border-color: #007ACC;
}
.question-message.answered .question-option:not(.selected) {
opacity: 0.5;
@ -380,7 +382,7 @@ export function getMessageAreaStyles(): string {
}
/* 低调显示的工具调用 - 移除边距和背景 */
.segment-tool.low-profile {
margin: 5px 0px;
margin: 25px 0px;
padding: 0;
background: none;
}
@ -420,6 +422,9 @@ export function getMessageAreaStyles(): string {
height: 100%;
display: block;
}
.icon-expanded svg path {
fill: #007ACC !important;
}
.tool-segment-header.collapsed .tool-collapse-icon {
transform: rotate(-90deg);
}
@ -544,9 +549,9 @@ export function getMessageAreaStyles(): string {
max-height: 0;
}
.tool-segment-description {
margin: 6px 0 0 0px;
margin: 25px 0 0 0px;
font-size: 0.9rem;
color: #ccc;
color: var(--vscode-descriptionForeground);
line-height: 1.4;
}
/* 低调显示的工具调用样式 */
@ -585,20 +590,22 @@ export function getMessageAreaStyles(): string {
}
.segment-question .question-option {
padding: 8px 16px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-button-border);
background: #3d3f41;
color: #ffffff;
border: 1px solid #474747;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
font-size: 13px;
}
.segment-question .question-option:hover {
background: var(--vscode-button-secondaryHoverBackground);
background: #005a9e;
border-color: #005a9e;
}
.segment-question .question-option.selected {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
background: #007ACC;
color: #ffffff;
border-color: #007ACC;
}
.segment-question.answered .question-option:not(.selected) {
opacity: 0.5;
@ -850,7 +857,26 @@ export function getMessageAreaScript(): string {
div.appendChild(actionsDiv);
} else {
div.textContent = text;
// 用户消息:解析文件路径并转换为标签
const parts = text.split(' ');
const filePaths = [];
const textParts = [];
parts.forEach(part => {
// 判断是否为文件路径或代码片段:包含路径分隔符、文件扩展名或代码片段格式(文件名:行号-行号)
if (part.includes('/') || part.includes('\\\\') || /\\.[a-zA-Z0-9]+$/.test(part) || /:[0-9]+-[0-9]+$/.test(part)) {
filePaths.push(part);
} else {
textParts.push(part);
}
});
if (filePaths.length > 0) {
div.innerHTML = filePaths.map(fp => window.createFilePathTag ? window.createFilePathTag(fp) : fp).join('') + ' ' + textParts.join(' ');
} else {
div.textContent = text;
}
// 当添加用户消息时,隐藏 header
hideHeaderIfNeeded();
}
@ -988,34 +1014,42 @@ export function getMessageAreaScript(): string {
// 实时更新分段消息(按后端返回顺序)
function updateSegmentsRealtime(segments, isComplete) {
console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete);
// 如果对话完成且没有新段落,只重置容器
if (isComplete && (!segments || segments.length === 0)) {
currentSegmentedMessage = null;
return;
}
if (!segments || segments.length === 0) {
console.log('[WebView] segments 为空,跳过渲染');
return;
}
// 如果没有当前分段消息容器,创建一个
if (!currentSegmentedMessage) {
console.log('[WebView] 创建新的分段消息容器');
// 移除流式消息(如果有)
if (currentStreamingMessage) {
console.log('[WebView] 移除流式消息');
currentStreamingMessage.remove();
currentStreamingMessage = null;
}
// 移除所有工具状态消息(因为会在分段中显示)
const toolStatuses = messagesEl.querySelectorAll('.tool-status');
console.log('[WebView] 找到工具状态消息数量:', toolStatuses.length);
toolStatuses.forEach(el => {
console.log('[WebView] 移除工具状态消息:', el.className);
el.remove();
});
currentSegmentedMessage = document.createElement('div');
currentSegmentedMessage.className = 'message bot-message segmented-message';
messagesEl.appendChild(currentSegmentedMessage);
// 检查最后一个容器是否是未完成的对话(没有操作按钮)
const lastSegmented = messagesEl.querySelector('.segmented-message:last-child');
if (lastSegmented && !lastSegmented.querySelector('.message-actions')) {
// 复用未完成的容器
currentSegmentedMessage = lastSegmented;
} else {
// 创建新容器
currentSegmentedMessage = document.createElement('div');
currentSegmentedMessage.className = 'message bot-message segmented-message';
messagesEl.appendChild(currentSegmentedMessage);
}
renderedSegmentCount = 0;
}
// 保存当前所有工具的展开/折叠状态
@ -1154,64 +1188,60 @@ export function getMessageAreaScript(): string {
} else if (segment.type === 'question') {
segmentDiv.className += ' segment-question';
// 兼容旧格式:如果有 segment.question转换为 questions 数组
const questions = segment.questions || (segment.question ? [{
question: segment.question,
options: segment.options || [],
multiSelect: false
}] : []);
// 检查是否已回答
const isAnswered = answeredQuestions.has(segment.askId);
const selectedAnswer = answeredQuestions.get(segment.askId);
const savedAnswers = answeredQuestions.get(segment.askId) || {};
if (isAnswered) {
segmentDiv.classList.add('answered');
}
// 检查是否有选项
const hasOptions = segment.options && segment.options.length > 0;
// 渲染多个问题
const questionsHtml = questions.map((q, qIndex) => {
const inputType = q.multiSelect ? 'checkbox' : 'radio';
const inputName = \`q\${qIndex}\`;
const selectedAnswers = savedAnswers[qIndex] || [];
const optionsHtml = hasOptions
? (segment.options || []).map(opt => {
const isSelected = isAnswered && opt === selectedAnswer;
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
}).join('')
: '';
const optionsHtml = q.options.map(opt => {
const isSelected = selectedAnswers.includes(opt);
return \`<label class="question-option\${isSelected ? ' selected' : ''}" style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:5px 5px 5px 0;">
<input type="\${inputType}" name="\${inputName}" value="\${opt}" \${isSelected ? 'checked' : ''} \${isAnswered ? 'disabled' : ''}>
<span>\${opt}</span>
</label>\`;
}).join('');
return \`
<div class="question-item" data-question-index="\${qIndex}" style="margin-bottom:12px;">
<div class="question-text" style="margin-bottom:8px;">\${formatText(q.question)}</div>
<div class="question-options">\${optionsHtml}</div>
</div>
\`;
}).join('');
segmentDiv.innerHTML = \`
<div class="question-text">\${formatText(segment.question || '')}</div>
\${hasOptions ? \`<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>\` : ''}
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
<input type="text" class="custom-input" placeholder="\${hasOptions ? '输入其他答案...' : '请输入您的答案...'}" />
<button class="custom-submit">提交</button>
</div>
\${questionsHtml}
<button class="custom-submit" style="display:\${isAnswered ? 'none' : 'block'};margin-top:8px;padding:8px 16px;background:var(--vscode-button-background);color:var(--vscode-button-foreground);border:none;border-radius:6px;cursor:pointer;">提交答案</button>
\`;
// 只在未回答时添加事件监听
if (!isAnswered) {
setTimeout(() => {
if (hasOptions) {
const optionButtons = segmentDiv.querySelectorAll('.question-option');
optionButtons.forEach(btn => {
btn.addEventListener('click', function() {
const option = this.getAttribute('data-option');
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
});
});
}
const submitBtn = segmentDiv.querySelector('.custom-submit');
const customInput = segmentDiv.querySelector('.custom-input');
if (submitBtn && customInput) {
if (submitBtn) {
submitBtn.addEventListener('click', function() {
const customValue = customInput.value.trim();
if (customValue) {
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
}
});
// 支持回车提交
customInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const customValue = customInput.value.trim();
if (customValue) {
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
}
}
const answers = {};
questions.forEach((q, qIndex) => {
const inputs = segmentDiv.querySelectorAll(\`input[name="q\${qIndex}"]:checked\`);
answers[qIndex] = Array.from(inputs).map(input => input.value);
});
handleMultiQuestionAnswer(segment.askId, answers, segmentDiv);
});
}
}, 0);
@ -1227,7 +1257,7 @@ export function getMessageAreaScript(): string {
currentSegmentedMessage.appendChild(segmentDiv);
});
// 如果对话完成,添加操作按钮
// 如果对话完成,添加操作按钮并重置容器
if (isComplete) {
console.log('[WebView] 对话完成,添加操作按钮');
const actionsDiv = document.createElement('div');
@ -1262,7 +1292,7 @@ export function getMessageAreaScript(): string {
actionsDiv.appendChild(dislikeBtn);
currentSegmentedMessage.appendChild(actionsDiv);
// 重置当前分段消息容器
// 重置当前分段消息容器(继续对话时创建新容器)
currentSegmentedMessage = null;
}
@ -1689,6 +1719,43 @@ export function getMessageAreaScript(): string {
});
}
// 处理多问题答案提交
function handleMultiQuestionAnswer(askId, answers, segmentDiv) {
console.log('[WebView] 多问题答案提交:', askId, answers);
// 保存答案到 Map 中
answeredQuestions.set(askId, answers);
// 标记问题已回答
segmentDiv.classList.add('answered');
// 禁用所有输入并保持选中状态的高亮
const inputs = segmentDiv.querySelectorAll('input');
inputs.forEach(input => {
input.disabled = true;
// 确保选中的选项保持高亮
if (input.checked) {
const label = input.closest('.question-option');
if (label) {
label.classList.add('selected');
}
}
});
// 隐藏提交按钮
const submitBtn = segmentDiv.querySelector('.custom-submit');
if (submitBtn) {
submitBtn.style.display = 'none';
}
// 发送答案到后端
vscode.postMessage({
command: 'submitAnswer',
askId: askId,
answers: answers
});
}
${getWaveformPreviewScript()}
${getCodeHighlightScript()}

View File

@ -9,66 +9,16 @@ export function getModelSelectorContent(
autoIcon: string = "",
liteIcon: string = "",
syIcon: string = "",
maxIcon: string = ""
maxIcon: string = "",
): 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 selected" data-value="auto" onclick="selectModel('auto', 'Auto')">
${
autoIcon
? `<img src="${autoIcon}" class="model-icon" alt="Auto">`
: ""
}
<div class="option-content">
<span class="option-label">Auto</span>
<span class="option-desc">智能匹配最优模型</span>
</div>
</div>
<div class="select-option" data-value="lite" onclick="selectModel('lite', 'Lite')">
${
liteIcon
? `<img src="${liteIcon}" class="model-icon" alt="Lite">`
: ""
}
<div class="option-content">
<span class="option-label">Lite</span>
<span class="option-desc">基础模型,快速相应,适合简单任务</span>
</div>
</div>
<div class="select-option" data-value="syntaxic" onclick="selectModel('syntaxic', 'Syntaxic')">
${
syIcon
? `<img src="${syIcon}" class="model-icon" alt="Syntaxic">`
: ""
}
<div class="option-content">
<span class="option-label">Syntaxic</span>
<span class="option-desc">均衡成本和性能节省credits同时保持可靠输出</span>
</div>
</div>
<div class="select-option" data-value="max" onclick="selectModel('max', 'Max')">
${
maxIcon
? `<img src="${maxIcon}" class="model-icon" alt="Max">`
: ""
}
<div class="option-content">
<span class="option-label">Max</span>
<span class="option-desc">最强性能,质量优先,适合复杂任务</span>
</div>
</div>
</div>
<div class="model-display">
<img src="${maxIcon || ""}" class="model-icon" alt="Max" style="display: ${maxIcon ? "block" : "none"};">
<span class="model-label">Max</span>
</div>
<span class="tooltiptext">选择模型</span>
<span class="tooltiptext">IC Coder自研FPGA专属微调模型</span>
</div>
`;
}
@ -78,72 +28,16 @@ export function getModelSelectorContent(
*/
export function getModelSelectorStyles(): string {
return `
/* 自定义下拉框样式 */
.custom-select {
position: relative;
user-select: none;
}
.select-trigger {
.model-display {
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: 8px 12px;
cursor: pointer;
transition: background 0.2s ease;
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
#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);
cursor: default;
}
.model-icon {
width: 16px;
@ -151,21 +45,7 @@ export function getModelSelectorStyles(): string {
flex-shrink: 0;
object-fit: contain;
}
.option-content {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
}
.option-label {
font-size: 13px;
color: var(--vscode-foreground);
font-weight: 500;
white-space: nowrap;
}
.option-desc {
font-size: 11px;
color: var(--vscode-descriptionForeground);
.model-label {
white-space: nowrap;
}
`;
@ -176,58 +56,9 @@ export function getModelSelectorStyles(): string {
*/
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');
}
});
// 获取当前选中的模型
// 获取当前选中的模型(固定为 max
function getCurrentModel() {
return currentModel;
return 'max';
}
`;
}

View File

@ -1,6 +1,6 @@
/**
* 更多选项组件
* 包含用户手册和用户反馈入口
* 包含用户手册入口
*/
/**
@ -28,40 +28,10 @@ export function getMoreOptionsComponentContent(): string {
<div class="option-desc">查看使用文档和帮助</div>
</div>
</div>
<div class="more-option-item" id="userFeedbackOption">
<div class="option-icon">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 12h-2v-2h2v2zm0-4h-2V6h2v4z" fill="currentColor"/>
</svg>
</div>
<div class="option-text">
<div class="option-label">用户反馈</div>
<div class="option-desc">提交问题和建议</div>
</div>
</div>
</div>
</div>
</div>
<!-- 用户反馈二维码弹窗 -->
<div class="feedback-qrcode-modal" id="feedbackQRCodeModal">
<div class="feedback-qrcode-overlay" onclick="closeFeedbackQRCode()"></div>
<div class="feedback-qrcode-content">
<div class="feedback-qrcode-header">
<span class="feedback-qrcode-title">用户反馈</span>
<button class="feedback-qrcode-close" onclick="closeFeedbackQRCode()">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/>
</svg>
</button>
</div>
<div class="feedback-qrcode-body">
<img class="feedback-qrcode-image" id="feedbackQRCodeImage" alt="微信二维码" />
<p class="feedback-qrcode-text">扫描二维码添加微信反馈</p>
</div>
</div>
</div>
</div>
`;
}
@ -163,125 +133,6 @@ export function getMoreOptionsComponentStyles(): string {
.option-desc {
display: none;
}
/* 用户反馈二维码弹窗 */
.feedback-qrcode-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 20000;
align-items: center;
justify-content: center;
}
.feedback-qrcode-modal.active {
display: flex;
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.feedback-qrcode-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
cursor: pointer;
}
.feedback-qrcode-content {
position: relative;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-widget-border);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
max-width: 400px;
width: 90%;
animation: slideUp 0.2s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.feedback-qrcode-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--vscode-widget-border);
}
.feedback-qrcode-title {
font-size: 14px;
font-weight: 600;
color: var(--vscode-foreground);
}
.feedback-qrcode-close {
width: 28px;
height: 28px;
padding: 0;
background: transparent;
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s ease;
}
.feedback-qrcode-close:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.feedback-qrcode-close svg {
width: 16px;
height: 16px;
color: var(--vscode-foreground);
}
.feedback-qrcode-body {
padding: 24px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.feedback-qrcode-image {
width: 200px;
height: 200px;
border: 1px solid var(--vscode-widget-border);
border-radius: 8px;
}
.feedback-qrcode-text {
margin: 0;
font-size: 13px;
color: var(--vscode-descriptionForeground);
text-align: center;
}
`;
}
@ -331,29 +182,6 @@ export function getMoreOptionsComponentScript(): string {
closeMoreOptionsDropdown();
}
// 打开用户反馈
function openUserFeedback() {
console.log('打开用户反馈');
vscode.postMessage({ command: 'openUserFeedback' });
closeMoreOptionsDropdown();
}
// 显示用户反馈二维码弹窗
function showFeedbackQRCode() {
const modal = document.getElementById('feedbackQRCodeModal');
if (modal) {
modal.classList.add('active');
}
}
// 关闭用户反馈二维码弹窗
function closeFeedbackQRCode() {
const modal = document.getElementById('feedbackQRCodeModal');
if (modal) {
modal.classList.remove('active');
}
}
// 绑定更多选项事件
document.addEventListener('DOMContentLoaded', () => {
// 绑定用户手册选项
@ -362,12 +190,6 @@ export function getMoreOptionsComponentScript(): string {
userManualOption.addEventListener('click', openUserManual);
}
// 绑定用户反馈选项
const userFeedbackOption = document.getElementById('userFeedbackOption');
if (userFeedbackOption) {
userFeedbackOption.addEventListener('click', openUserFeedback);
}
// 点击页面其他地方关闭下拉面板
document.addEventListener('click', (e) => {
const dropdown = document.getElementById('moreOptionsDropdown');

View File

@ -18,7 +18,7 @@ export function getNdtWelcomeModalContent(logoUri?: string): string {
<div class="ndt-welcome-modal-header">
<div class="ndt-welcome-icon">🎉</div>
<h2>欢迎宁德时代新能源科技股份有限公司<span style="white-space: nowrap;">的各位专家</span>使用 IC Coder</h2>
<h2>欢迎企业<span style="white-space: nowrap;">的各位专家</span>使用 IC Coder</h2>
</div>
<div class="ndt-welcome-modal-body">

View File

@ -195,11 +195,11 @@ export function getPlanCardStyles(): string {
background: var(--vscode-list-hoverBackground);
}
.plan-btn-confirm {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
background: #007ACC;
color: #ffffff;
}
.plan-btn-confirm:hover {
background: var(--vscode-button-hoverBackground);
background: #005a9e;
}
.plan-btn-cancel {
background: transparent;

View File

@ -25,7 +25,7 @@ export function getProgressBarContent(): string {
<span class="step-number">1</span>
<span class="step-check">✓</span>
</div>
<div class="step-label">Spec</div>
<div class="step-label">Specification</div>
</div>
<div class="progress-line"></div>
@ -186,8 +186,8 @@ export function getProgressBarStyles(): string {
/* 已完成状态 */
.progress-step.completed .step-circle {
background: var(--vscode-button-background);
border-color: var(--vscode-button-background);
background: #007ACC;
border-color: #007ACC;
}
.progress-step.completed .step-number {
@ -204,14 +204,14 @@ export function getProgressBarStyles(): string {
}
.progress-step.completed + .progress-line {
background: var(--vscode-button-background);
background: #007ACC;
}
/* 进行中状态 */
.progress-step.active .step-circle {
background: var(--vscode-button-background);
border-color: var(--vscode-button-background);
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
background: #007ACC;
border-color: #007ACC;
box-shadow: 0 0 0 2px #007ACC33;
animation: pulse 2s infinite;
}
@ -226,10 +226,10 @@ export function getProgressBarStyles(): string {
@keyframes pulse {
0%, 100% {
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
box-shadow: 0 0 0 2px #007ACC33;
}
50% {
box-shadow: 0 0 0 4px var(--vscode-button-background)1a;
box-shadow: 0 0 0 4px #007ACC1a;
}
}
@ -351,7 +351,7 @@ export function getProgressBarScript(): string {
// 更新连接线
document.querySelectorAll('.progress-line').forEach((line, index) => {
if (index < currentIndex) {
line.style.background = 'var(--vscode-button-background)';
line.style.background = '#007ACC';
} else {
line.style.background = 'var(--vscode-input-border)';
}

View File

@ -3,11 +3,6 @@ import {
getGeneralSettingsComponentStyles,
getGeneralSettingsComponentScript,
} from "./generalSettingsComponent";
import {
getRulesSettingsComponentContent,
getRulesSettingsComponentStyles,
getRulesSettingsComponentScript,
} from "./rulesSettingsComponent";
/**
* 获取设置面板的 HTML 内容
@ -31,18 +26,13 @@ export function getSettingsComponentContent(): string {
<button class="settings-nav-item active" data-tab="general" onclick="switchSettingsTab('general')">
通用
</button>
<button class="settings-nav-item" data-tab="rules" onclick="switchSettingsTab('rules')">
规则
</button>
</div>
<div class="settings-content">
<div class="settings-tab-content active" id="generalSettings">
${getGeneralSettingsComponentContent()}
</div>
<div class="settings-tab-content" id="rulesSettings">
${getRulesSettingsComponentContent()}
</div>
</div>
</div>
</div>
@ -186,7 +176,6 @@ export function getSettingsComponentStyles(): string {
}
${getGeneralSettingsComponentStyles()}
${getRulesSettingsComponentStyles()}
`;
}
@ -196,13 +185,14 @@ export function getSettingsComponentStyles(): string {
export function getSettingsComponentScript(): string {
return `
${getGeneralSettingsComponentScript()}
${getRulesSettingsComponentScript()}
// 打开设置面板
function openSettingsModal() {
const modal = document.getElementById('settingsModal');
if (modal) {
modal.classList.add('active');
// 请求加载设置
vscode.postMessage({ command: 'loadGeneralSettings' });
}
}

View File

@ -9,57 +9,7 @@
*/
export function getUserInfoComponentContent(): string {
return `
<div class="user-info-wrapper">
<!-- 用户详情下拉面板 -->
<div class="user-detail-dropdown" id="userDetailDropdown">
<div class="user-detail-content">
<div class="user-detail-header">
<div class="user-info-row">
<div class="user-avatar-small clickable" id="userAvatarClickable">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"/>
</svg>
</div>
<div class="user-name-tier">
<div class="user-detail-name clickable" id="userDetailName">加载中...</div>
<img class="tier-icon-inline" id="tierIconInline" style="display: none;" />
</div>
</div>
<!-- 升级到Pro按钮 (仅BASIC会员显示) -->
<!-- <div class="upgrade-pro-wrapper" id="upgradeProWrapper" style="display: none;">
<button class="upgrade-pro-btn" id="upgradeProBtn">升级到 Pro</button>
</div> -->
</div>
<div class="user-detail-body">
<!-- <div class="user-detail-item">
<span class="detail-label">剩余 Credits</span>
<span class="detail-value" id="creditsDetail">-</span>
</div> -->
<div class="user-detail-item logout-item" id="logoutItem">
<span class="detail-label">账户管理</span>
<span class="detail-value logout-link">退出登录</span>
</div>
</div>
</div>
</div>
</div>
<!-- 退出登录确认对话框 -->
<div class="logout-confirm-modal" id="logoutConfirmModal">
<div class="logout-confirm-overlay"></div>
<div class="logout-confirm-content">
<div class="logout-confirm-header">
<h3>确认退出</h3>
</div>
<div class="logout-confirm-body">
<p>确定要退出登录吗?</p>
</div>
<div class="logout-confirm-footer">
<button class="logout-confirm-btn logout-cancel-btn" id="logoutCancelBtn">取消</button>
<button class="logout-confirm-btn logout-ok-btn" id="logoutOkBtn">确定</button>
</div>
</div>
<div class="user-info-wrapper" style="display: none;">
</div>
`;
}

View File

@ -18,6 +18,11 @@ import {
getMessageAreaScript,
} from "./messageArea";
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
import {
getMoreOptionsComponentContent,
getMoreOptionsComponentStyles,
getMoreOptionsComponentScript,
} from "./moreOptionsComponent";
import {
getProgressBarContent,
getProgressBarStyles,
@ -110,6 +115,7 @@ export function getWebviewContent(
}
${getMessageAreaStyles()}
${getAgentCardStyles()}
${getMoreOptionsComponentStyles()}
${getWaveformPreviewContent()}
${getConversationHistoryBarStyles()}
${getProgressBarStyles()}
@ -375,16 +381,32 @@ export function getWebviewContent(
font-size: 13px;
color: var(--vscode-descriptionForeground);
}
.status-bar #statusText {
background: linear-gradient(90deg,
var(--vscode-descriptionForeground) 0%,
var(--vscode-foreground) 50%,
var(--vscode-descriptionForeground) 100%);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: textShimmer 2s linear infinite;
}
@keyframes textShimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--vscode-charts-blue);
animation: statusPulse 1.5s ease-in-out infinite;
box-shadow: 0 0 8px currentColor;
}
@keyframes statusPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.8); }
0%, 100% { opacity: 1; transform: scale(1.2); }
50% { opacity: 0.3; transform: scale(0.6); }
}
.status-bar.working .status-indicator {
background: var(--vscode-charts-orange);
@ -488,13 +510,17 @@ export function getWebviewContent(
${getNdtWelcomeModalContent(logoUri)}
${getExpiredModalContent(logoUri)}
<div class="header">
<div style="display: flex; align-items: center; justify-content: center;">
<div style="display: flex; align-items: flex-end; justify-content: center">
<img src="${logoUri}" alt="IC Coder" style="max-width: 100%; height: auto; max-height: 80px;" />
<span style="font-size: 23px; font-weight: bold; background: linear-gradient(to bottom, #b2e4ff, #42bcff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin: 0 0 14px -16px;">企业版</span>
</div>
<p style="font-size: 16px; margin-top: 12px; line-height: 1.5;">
The <span style="background: linear-gradient(to right, #42bcff, #4A9EFF); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: bold;">Agentic AI</span> Verilog Coding Platform,
<span style="display: block; margin-top: 8px;">将芯片设计与验证的效率提升至少20倍</span>
<p style="font-size: 16px; margin-top: 8px; line-height: 1.5;">
The <span style="background: linear-gradient(to right, #42bcff, #4A9EFF); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: bold;">Agentic AI</span> Verilog Coding Platform
<span style="display: block; margin-top: 8px;">将FPGA研发效率提升至少20倍</span>
</p>
<div style="margin-top: 16px; padding: 8px 20px; background: linear-gradient(135deg, rgba(255, 215, 0, 0.15), rgba(255, 165, 0, 0.15)); border: 1px solid rgba(255, 215, 0, 0.3); border-radius: 6px;">
<p style="font-size: 13px; margin: 0; background: linear-gradient(135deg, #FFD700, #FFA500); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 600; letter-spacing: 1px;">宁德时代专属定制版</p>
</div>
</div>
<div class="chat-container">
@ -754,6 +780,11 @@ export function getWebviewContent(
hideLoadingIndicator();
break;
case 'taskComplete':
// 显示任务完成提示
addMessage('✅ 任务已完成', 'bot');
break;
case 'workspaceStatus':
// 更新工作区状态
if (typeof hasWorkspace !== 'undefined') {
@ -903,6 +934,13 @@ export function getWebviewContent(
}
break;
case 'loadedGeneralSettings':
// 加载通用设置
if (typeof loadGeneralSettings === 'function') {
loadGeneralSettings(message.settings);
}
break;
default:
console.log('[WebView] 未处理的消息类型:', message.command);
}
@ -910,6 +948,7 @@ export function getWebviewContent(
${getMessageAreaScript()}
${getAgentCardScript()}
${getMoreOptionsComponentScript()}
${getWaveformPreviewScript()}
${getConversationHistoryBarScript()}
${getProgressBarScript()}

View File

@ -19,40 +19,41 @@ export function getWelcomeModalContent(logoUri?: string): string {
<div class="welcome-modal-header">
<div class="welcome-icon">🎉</div>
<h2>欢迎使用 IC Coder</h2>
<p class="welcome-modal-subtitle">您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!</p>
</div>
<div class="welcome-modal-body">
<div class="welcome-step">
<div class="welcome-step-icon">📝</div>
<div class="welcome-step-content">
<h3>步骤 1打开聊天面板</h3>
<p>点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"</p>
<!-- 试用期提示 -->
<div class="trial-banner">
<span>您已获得 <strong>5 天企业版试用期</strong>企业版试用期内Credits用量无限并可无限制使用所有功能</span>
</div>
<!-- IC Coder 简介 -->
<div class="intro-section">
<h3 class="section-title">关于 IC Coder</h3>
<p class="intro-text">IC Coder是一款The Agentic AI Verilog Coding Platform自主式人工智能 Verilog 编码平台。我们采用全球顶尖的IC Coder自研芯片设计微调模型为代码生成提供强大的AI能力支撑。</p>
<div class="features">
<div class="feature-item">
<span class="feature-text">多智能体架构Multi-Agent System多个专业化AI智能体协同工作分别负责架构设计、代码生成、验证测试等不同环节</span>
</div>
<div class="feature-item">
<span class="feature-text">增强上下文引擎:智能理解和管理大规模设计上下文,确保生成代码的一致性和准确性</span>
</div>
<div class="feature-item">
<span class="feature-text">AI自主仿真IC Coder提供完全自动化的仿真验证流程无需手动编写测试代码</span>
</div>
</div>
</div>
<div class="welcome-step">
<div class="welcome-step-icon">💬</div>
<div class="welcome-step-content">
<h3>步骤 2输入您的需求</h3>
<p>描述您想要生成的 Verilog 代码或需要帮助的问题AI 将为您提供专业的解决方案</p>
</div>
<!-- 按钮组 -->
<div class="button-group">
<button id="welcomeStartBtn" class="welcome-btn welcome-btn-primary">
<span>开始使用</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<div class="welcome-step">
<div class="welcome-step-icon">🔬</div>
<div class="welcome-step-content">
<h3>步骤 3运行仿真</h3>
<p>使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果</p>
</div>
</div>
<button id="welcomeStartBtn" class="welcome-btn welcome-btn-primary">
<span>开始使用</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
</div>
@ -96,7 +97,7 @@ export function getWelcomeModalStyles(): string {
border: 1px solid var(--vscode-widget-border);
border-radius: 12px;
width: 100%;
max-width: 500px;
max-width: 480px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
overflow: hidden;
@ -124,9 +125,10 @@ export function getWelcomeModalStyles(): string {
.welcome-modal-header h2 {
margin: 0 0 12px;
font-size: 24px;
font-size: 20px;
font-weight: 600;
color: var(--vscode-foreground);
line-height: 1.4;
}
.welcome-modal-subtitle {
@ -140,37 +142,70 @@ export function getWelcomeModalStyles(): string {
padding: 0 32px 32px;
}
.welcome-step {
/* 试用期横幅 */
.trial-banner {
display: flex;
gap: 16px;
margin: 20px 0;
padding: 16px;
align-items: center;
justify-content: center;
padding: 12px 16px;
background: var(--vscode-editor-inactiveSelectionBackground);
border-radius: 8px;
border-left: 4px solid var(--vscode-textLink-foreground);
}
.welcome-step-icon {
font-size: 24px;
flex-shrink: 0;
}
.welcome-step-content h3 {
margin: 0 0 8px;
font-size: 15px;
font-weight: 600;
color: var(--vscode-textLink-foreground);
}
.welcome-step-content p {
margin: 0;
margin-bottom: 20px;
font-size: 13px;
color: var(--vscode-descriptionForeground);
line-height: 1.5;
border-left: 3px solid var(--vscode-textLink-foreground);
}
.trial-banner strong {
color: var(--vscode-textLink-foreground);
font-weight: 600;
}
/* IC Coder 简介区域 */
.intro-section {
margin-bottom: 24px;
}
.section-title {
margin: 0 0 12px;
font-size: 15px;
font-weight: 600;
color: var(--vscode-foreground);
}
.intro-text {
margin: 0 0 16px;
font-size: 13px;
color: var(--vscode-descriptionForeground);
line-height: 1.6;
}
.features {
display: flex;
flex-direction: column;
gap: 10px;
}
.feature-item {
padding: 10px 12px;
background: var(--vscode-editor-inactiveSelectionBackground);
border-radius: 6px;
font-size: 13px;
color: var(--vscode-foreground);
border-left: 2px solid var(--vscode-textLink-foreground);
}
.feature-text {
display: block;
}
/* 按钮组 */
.button-group {
display: flex;
}
.welcome-btn {
width: 100%;
flex: 1;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
@ -181,18 +216,32 @@ export function getWelcomeModalStyles(): string {
align-items: center;
justify-content: center;
gap: 8px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
transition: all 0.2s;
margin-top: 24px;
}
.welcome-btn:hover {
.welcome-btn-primary {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}
.welcome-btn-primary:hover {
background: var(--vscode-button-hoverBackground);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.welcome-btn-secondary {
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-button-border);
}
.welcome-btn-secondary:hover {
background: var(--vscode-button-secondaryHoverBackground);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.welcome-btn:active {
transform: translateY(0);
}

Binary file not shown.

View File

@ -3,30 +3,30 @@
'use strict';
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
//@ts-check
/** @typedef {import('webpack').Configuration} WebpackConfig **/
/** @type WebpackConfig */
const extensionConfig = {
target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
target: 'node',
mode: process.env.NODE_ENV === 'production' ? 'production' : 'none',
entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
entry: './src/extension.ts',
output: {
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
path: path.resolve(__dirname, 'dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2'
libraryTarget: 'commonjs2',
clean: true // 自动清理旧文件
},
externals: {
vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
'node-notifier': 'commonjs node-notifier' // node-notifier 依赖原生模块,必须排除
// modules added here also need to be added in the .vscodeignore file
vscode: 'commonjs vscode',
'node-notifier': 'commonjs node-notifier'
},
resolve: {
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
extensions: ['.ts', '.js']
extensions: ['.ts', '.js'],
mainFields: ['module', 'main']
},
module: {
rules: [
@ -35,15 +35,37 @@ const extensionConfig = {
exclude: /node_modules/,
use: [
{
loader: 'ts-loader'
loader: 'ts-loader',
options: {
transpileOnly: true, // 加快编译速度
compilerOptions: {
sourceMap: true
}
}
}
]
}
]
},
devtool: 'nosources-source-map',
devtool: process.env.NODE_ENV === 'production' ? 'hidden-source-map' : 'nosources-source-map',
infrastructureLogging: {
level: "log", // enables logging required for problem matchers
level: "log",
},
plugins: [
new CopyWebpackPlugin({
patterns: [
{ from: 'src/assets', to: 'assets' }
]
})
],
optimization: {
minimize: process.env.NODE_ENV === 'production',
usedExports: true // Tree Shaking
},
performance: {
hints: 'warning',
maxAssetSize: 2 * 1024 * 1024, // 2MB
maxEntrypointSize: 2 * 1024 * 1024
}
};
module.exports = [ extensionConfig ];