Compare commits
20 Commits
eb345e3e1f
...
feat/eda
| Author | SHA1 | Date | |
|---|---|---|---|
| ccbc40f5fb | |||
| 5adde3d40a | |||
| fa5c2cdafd | |||
| aa80088abc | |||
| 0ae627ca7c | |||
| 7732b11d37 | |||
| 81717dc84f | |||
| 11c408ce0f | |||
| c138406217 | |||
| 2a280aaa93 | |||
| 2f6eae9f2b | |||
| d0ff876ba2 | |||
| 7fe87e515b | |||
| 790110ba7e | |||
| 29e80ce296 | |||
| c244a308d7 | |||
| 7cde4fa138 | |||
| 1b7259d1c1 | |||
| 09ff812562 | |||
| e7c631d532 |
@ -18,10 +18,15 @@ node_modules/**
|
||||
|
||||
# 文档(避免中文文件名打包问题)
|
||||
docs/**
|
||||
PUBLISH.md
|
||||
CLAUDE.md
|
||||
|
||||
# 只排除 waveform_trace 的 src/dist 目录
|
||||
tools/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/**
|
||||
|
||||
526
docs/EDA联动功能需求文档.md
Normal file
526
docs/EDA联动功能需求文档.md
Normal file
@ -0,0 +1,526 @@
|
||||
# Vivado 联动功能需求文档
|
||||
|
||||
## 1. 项目背景
|
||||
|
||||
### 1.1 当前状态
|
||||
|
||||
IC Coder Plugin 目前支持:
|
||||
|
||||
- iverilog 仿真(内置 Windows 版本)
|
||||
- VCD 波形查看
|
||||
- Verilog 代码生成和文件操作
|
||||
|
||||
### 1.2 需求来源
|
||||
|
||||
用户需要在 VS Code 中直接调用本地 Vivado 工具,并将产出文件自动导入到项目中,完成从仿真到 FPGA 部署的完整流程。
|
||||
|
||||
### 1.3 Vivado 是什么?
|
||||
|
||||
**Vivado** 是 Xilinx(现 AMD)的 FPGA 开发工具,用于将 Verilog 代码部署到 FPGA 硬件:
|
||||
|
||||
- **综合(Synthesis)**:将 RTL 代码转换为门级网表
|
||||
- **实现(Implementation)**:布局布线,映射到具体 FPGA 芯片
|
||||
- **生成比特流(Bitstream)**:生成 .bit 配置文件用于烧录
|
||||
|
||||
**与 iverilog 的区别**:
|
||||
|
||||
- iverilog:只做**仿真验证**(软件层面验证逻辑)
|
||||
- Vivado:做**综合+实现+生成配置文件**(真正部署到硬件)
|
||||
|
||||
**典型开发流程**:
|
||||
|
||||
```
|
||||
编写 Verilog → iverilog 仿真验证 → Vivado 综合 → Vivado 实现 → 生成 .bit 文件 → 烧录到 FPGA
|
||||
```
|
||||
|
||||
## 2. 功能目标
|
||||
|
||||
### 2.1 核心目标
|
||||
|
||||
- **前端提供原子工具**:前端只提供独立的 Vivado 命令工具,不控制流程
|
||||
- **后端AI控制流程**:所有执行顺序、依赖检查由后端AI决策
|
||||
- **工具职责单一**:每个工具只负责执行一个具体命令
|
||||
- **结果透明返回**:执行结果完整返回给后端,由后端决定下一步
|
||||
|
||||
### 2.2 设计原则
|
||||
|
||||
- 前端不做流程判断,只执行命令
|
||||
- 前端不检查依赖关系,由后端保证顺序
|
||||
- 前端返回详细的执行结果,包括成功/失败、输出、报告等
|
||||
- 后端AI根据结果智能决策是否继续
|
||||
|
||||
## 3. 功能详细需求
|
||||
|
||||
### 3.1 前端提供的工具
|
||||
|
||||
前端提供 4 个独立的工具,每个工具只负责执行一个命令:
|
||||
|
||||
#### 3.1.1 createVivadoProject - 创建工程
|
||||
|
||||
- **输入**:项目名称、芯片型号、源文件列表、约束文件(可选)
|
||||
- **输出**:工程文件(.xpr)
|
||||
- **说明**:创建 Vivado 工程,不执行任何构建操作
|
||||
|
||||
#### 3.1.2 runVivadoSynthesis - 综合
|
||||
|
||||
- **输入**:工程路径或源文件、芯片型号、顶层模块、约束文件(可选)
|
||||
- **输出**:.dcp 文件、综合报告
|
||||
- **说明**:执行综合,前端不检查工程是否存在。约束文件在此阶段可选,主要用于时序约束
|
||||
|
||||
#### 3.1.3 runVivadoImplementation - 实现
|
||||
|
||||
- **输入**:综合后的 .dcp 文件路径、约束文件(必需,包含管脚约束)
|
||||
- **输出**:实现后的 .dcp 文件、时序报告
|
||||
- **说明**:执行实现,前端不检查 .dcp 是否存在。**管脚约束是必需的**,否则无法完成布局布线
|
||||
|
||||
#### 3.1.4 runVivadoBitstream - 生成比特流
|
||||
|
||||
- **输入**:实现后的 .dcp 文件路径
|
||||
- **输出**:.bit 文件(可下载到 FPGA 的配置文件)
|
||||
- **核心依赖**:
|
||||
1. 实现已完成
|
||||
2. 工程指定目标芯片型号
|
||||
3. 已完成管脚约束(无管脚约束无法生成)
|
||||
- **说明**:生成比特流,前端不检查 .dcp 是否存在
|
||||
|
||||
### 3.2 配置管理
|
||||
|
||||
#### 3.2.1 配置项
|
||||
|
||||
```json
|
||||
{
|
||||
"vivado": {
|
||||
"enabled": true,
|
||||
"executablePath": "C:/Xilinx/Vivado/2023.1/bin/vivado.bat",
|
||||
"workingDir": "${workspaceFolder}/vivado_project",
|
||||
"part": "xc7a35tcpg236-1", // FPGA 型号
|
||||
"commands": {
|
||||
"synthesis": "vivado -mode batch -source synth.tcl",
|
||||
"implementation": "vivado -mode batch -source impl.tcl",
|
||||
"bitstream": "vivado -mode batch -source bitstream.tcl"
|
||||
},
|
||||
"outputFiles": {
|
||||
"synthesis": ["*.dcp", "*_synth.rpt"],
|
||||
"implementation": ["*.dcp", "*_timing.rpt", "*_utilization.rpt"],
|
||||
"bitstream": ["*.bit"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.2 存储位置
|
||||
|
||||
- 全局配置:VS Code Settings(`settings.json`)
|
||||
- 项目配置:`.vscode/ic-coder-vivado.json`(优先级更高)
|
||||
|
||||
### 3.3 工具调用接口
|
||||
|
||||
#### 3.3.1 通用响应格式
|
||||
|
||||
所有工具返回统一的响应格式:
|
||||
|
||||
```typescript
|
||||
interface VivadoToolResponse {
|
||||
success: boolean; // 是否成功
|
||||
command: string; // 执行的命令
|
||||
executionTime: number; // 执行时间(毫秒)
|
||||
output: string; // 完整输出日志
|
||||
error?: string; // 错误信息(如果失败)
|
||||
outputFiles?: string[]; // 产出文件路径列表
|
||||
reports?: {
|
||||
resources?: string; // 资源使用摘要
|
||||
timing?: string; // 时序信息摘要
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3.2 各工具的参数定义
|
||||
|
||||
**createVivadoProject**
|
||||
```typescript
|
||||
{
|
||||
projectName: string; // 项目名称
|
||||
part: string; // 芯片型号
|
||||
topModule: string; // 顶层模块
|
||||
files: string[]; // 源文件列表
|
||||
constraints?: string; // 约束文件(可选)
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
|
||||
**runVivadoSynthesis**
|
||||
```typescript
|
||||
{
|
||||
projectPath?: string; // 工程路径(可选,如果有工程)
|
||||
part: string; // 芯片型号
|
||||
topModule: string; // 顶层模块
|
||||
files?: string[]; // 源文件(如果没有工程)
|
||||
constraints?: string; // 约束文件(可选)
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
|
||||
**runVivadoImplementation**
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 综合后的 .dcp 文件路径
|
||||
constraints: string; // 约束文件(必需,包含管脚约束)
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
|
||||
**runVivadoBitstream**
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 实现后的 .dcp 文件路径
|
||||
mode: 'gui' | 'batch'; // 执行模式
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 后端AI的职责
|
||||
|
||||
后端AI负责:
|
||||
1. 询问用户必要参数(芯片型号、执行模式等)
|
||||
2. 理解用户意图,决定调用哪些工具
|
||||
3. 按正确顺序调用工具(遵循依赖关系)
|
||||
4. 检查每步执行结果,决定是否继续
|
||||
5. 汇总结果并展示给用户
|
||||
|
||||
#### 3.4.1 询问用户参数
|
||||
|
||||
后端必须询问:
|
||||
- **芯片型号**(必需):"请提供 FPGA 芯片型号(例如:xc7a35tcpg236-1)"
|
||||
- **执行模式**(必需):"选择执行模式:1) 图形化 2) 后端执行"
|
||||
- **约束文件**(必需):"请提供约束文件(.xdc),包含管脚约束和时序约束"
|
||||
|
||||
#### 3.4.2 理解依赖关系
|
||||
|
||||
后端AI需要理解:
|
||||
```
|
||||
创建工程 → 综合 → 实现 → 生成比特流
|
||||
```
|
||||
|
||||
如果用户说"做实现",后端应该:
|
||||
1. 先调用 `createVivadoProject` 创建工程
|
||||
2. 再调用 `runVivadoSynthesis` 执行综合
|
||||
3. 最后调用 `runVivadoImplementation` 执行实现
|
||||
|
||||
#### 3.4.3 逐步调用工具
|
||||
|
||||
```
|
||||
步骤1: 调用 createVivadoProject
|
||||
检查 response.success
|
||||
如果失败 → 停止并报错
|
||||
|
||||
步骤2: 调用 runVivadoSynthesis
|
||||
检查 response.success
|
||||
如果失败 → 停止并报错
|
||||
|
||||
步骤3: 调用 runVivadoImplementation
|
||||
检查 response.success
|
||||
返回最终结果
|
||||
```
|
||||
|
||||
### 3.5 UI 交互
|
||||
|
||||
#### 3.5.1 配置界面
|
||||
|
||||
- 在设置页面添加 "Vivado 配置" 选项
|
||||
- 支持配置 Vivado 路径、FPGA 型号
|
||||
- 支持测试 Vivado 可用性(点击按钮测试)
|
||||
|
||||
#### 3.5.2 调用界面
|
||||
|
||||
- 在聊天面板中,AI 可以建议使用 Vivado
|
||||
- 用户确认后,显示执行进度对话框
|
||||
- 实时显示日志输出(可折叠)
|
||||
- 显示执行状态:准备中 → 执行中 → 完成/失败
|
||||
|
||||
#### 3.5.3 结果展示
|
||||
|
||||
- 执行成功:显示执行时间、资源使用、时序信息
|
||||
- 执行失败:显示错误信息、建议解决方案
|
||||
- 导入文件:高亮显示已导入的文件,支持点击打开报告
|
||||
|
||||
### 3.6 后端集成
|
||||
|
||||
#### 3.6.1 工具定义
|
||||
|
||||
后端注册 4 个独立工具:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "createVivadoProject",
|
||||
"description": "创建 Vivado 工程。需要先询问用户芯片型号和执行模式。",
|
||||
"parameters": {
|
||||
"projectName": "项目名称",
|
||||
"part": "芯片型号(必须从用户获取)",
|
||||
"topModule": "顶层模块名",
|
||||
"files": "源文件列表",
|
||||
"constraints": "约束文件(可选)",
|
||||
"mode": "执行模式(gui/batch)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "runVivadoSynthesis",
|
||||
"description": "执行 Vivado 综合。前端不检查依赖,后端需确保工程已创建。",
|
||||
"parameters": {
|
||||
"projectPath": "工程路径(可选)",
|
||||
"part": "芯片型号",
|
||||
"topModule": "顶层模块",
|
||||
"files": "源文件(如果没有工程)",
|
||||
"constraints": "约束文件(可选)",
|
||||
"mode": "执行模式(gui/batch)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "runVivadoImplementation",
|
||||
"description": "执行 Vivado 实现。前端不检查依赖,后端需确保综合已完成且提供约束文件。",
|
||||
"parameters": {
|
||||
"dcpFile": "综合后的 .dcp 文件路径",
|
||||
"constraints": "约束文件(必需,包含管脚约束)",
|
||||
"mode": "执行模式(gui/batch)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "runVivadoBitstream",
|
||||
"description": "生成比特流。前端不检查依赖,后端需确保实现已完成。",
|
||||
"parameters": {
|
||||
"dcpFile": "实现后的 .dcp 文件路径",
|
||||
"mode": "执行模式(gui/batch)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.6.2 后端调用示例
|
||||
|
||||
**场景:用户要求完整流程**
|
||||
|
||||
```
|
||||
用户: 用 Vivado 跑完整流程
|
||||
|
||||
AI: 请提供芯片型号和执行模式
|
||||
用户: xc7a35tcpg236-1,后端执行
|
||||
|
||||
AI 执行:
|
||||
1. [调用] createVivadoProject({ projectName: "counter", part: "xc7a35tcpg236-1", ... })
|
||||
[结果] { success: true, outputFiles: ["counter.xpr"] }
|
||||
|
||||
2. [调用] runVivadoSynthesis({ projectPath: "counter.xpr", ... })
|
||||
[结果] { success: true, outputFiles: ["counter_synth.dcp"], reports: {...} }
|
||||
|
||||
3. [调用] runVivadoImplementation({ dcpFile: "counter_synth.dcp", constraints: "counter.xdc", ... })
|
||||
[结果] { success: true, outputFiles: ["counter_impl.dcp"], reports: {...} }
|
||||
|
||||
4. [调用] runVivadoBitstream({ dcpFile: "counter_impl.dcp", ... })
|
||||
[结果] { success: true, outputFiles: ["counter.bit"] }
|
||||
|
||||
AI 回复: 完成!已生成 counter.bit 文件
|
||||
```
|
||||
|
||||
## 4. 用户场景
|
||||
|
||||
### 4.1 场景一:完整流程执行
|
||||
|
||||
**用户输入**:"用 Vivado 跑完整个 FPGA 流程"
|
||||
|
||||
**AI 交互**:
|
||||
```
|
||||
AI: 好的,我将使用 Vivado 执行完整的 FPGA 开发流程。请提供以下信息:
|
||||
|
||||
1. FPGA 芯片型号(例如:xc7a35tcpg236-1)
|
||||
2. 请选择要执行的步骤(可多选):
|
||||
- 综合(Synthesis)
|
||||
- 实现(Implementation)
|
||||
- 生成比特流(Bitstream)
|
||||
3. 执行模式:
|
||||
- 图形化模式:打开 Vivado GUI
|
||||
- 后端执行:自动在后台执行
|
||||
4. 约束文件路径(.xdc,必需)
|
||||
|
||||
用户: xc7a35tcpg236-1,全部步骤,后端执行,约束文件是 counter.xdc
|
||||
|
||||
AI: 收到!执行计划:
|
||||
1. 创建工程
|
||||
2. 综合
|
||||
3. 实现
|
||||
4. 生成比特流
|
||||
|
||||
开始执行...
|
||||
|
||||
[步骤 1/4] 创建工程...
|
||||
✓ 工程创建成功(耗时 2s)
|
||||
|
||||
[步骤 2/4] 综合中...
|
||||
✓ 综合完成(耗时 45s)
|
||||
资源使用:LUT 32/20800 (0.15%), FF 8/41600 (0.02%)
|
||||
|
||||
[步骤 3/4] 实现中...
|
||||
✓ 实现完成(耗时 120s)
|
||||
时序:WNS 5.234ns, TNS 0.000ns(满足要求)
|
||||
|
||||
[步骤 4/4] 生成比特流...
|
||||
✓ 比特流生成完成(耗时 15s)
|
||||
|
||||
所有步骤执行完毕!产出文件已导入到 vivado_output/ 目录。
|
||||
```
|
||||
|
||||
### 4.2 场景二:只执行实现(自动补充依赖)
|
||||
|
||||
**用户输入**:"用 Vivado 做实现"
|
||||
|
||||
**AI 交互**:
|
||||
```
|
||||
AI: 请提供以下信息:
|
||||
1. FPGA 芯片型号
|
||||
2. 执行模式(图形化/后端执行)
|
||||
|
||||
用户: xc7a35tcpg236-1,后端执行
|
||||
|
||||
AI: 注意:实现依赖综合,我将自动执行以下步骤:
|
||||
1. 创建工程
|
||||
2. 综合(前置依赖)
|
||||
3. 实现(您选择的步骤)
|
||||
|
||||
是否继续?
|
||||
|
||||
用户: 是
|
||||
|
||||
AI: 开始执行...
|
||||
[自动执行创建工程 → 综合 → 实现]
|
||||
```
|
||||
|
||||
### 4.3 场景三:图形化模式
|
||||
|
||||
**用户输入**:"打开 Vivado 图形界面做综合"
|
||||
|
||||
**AI 交互**:
|
||||
```
|
||||
AI: 请提供芯片型号:
|
||||
用户: xc7a35tcpg236-1
|
||||
|
||||
AI: 正在创建工程并打开 Vivado GUI...
|
||||
✓ 工程已创建
|
||||
✓ Vivado 图形界面已启动
|
||||
|
||||
您现在可以在 Vivado 中手动操作。完成后,产出文件将保存在工程目录中。
|
||||
```
|
||||
|
||||
### 4.4 场景四:执行中断处理
|
||||
|
||||
**执行过程**:
|
||||
```
|
||||
[步骤 1/3] 创建工程...
|
||||
✓ 成功
|
||||
|
||||
[步骤 2/3] 综合中...
|
||||
✗ 综合失败
|
||||
错误:ERROR: [Synth 8-439] module 'counter' not found
|
||||
|
||||
AI: 综合失败,发现以下问题:
|
||||
- 找不到模块 'counter'
|
||||
|
||||
建议检查:
|
||||
1. 模块名是否正确
|
||||
2. 文件中是否定义了该模块
|
||||
3. 是否有语法错误
|
||||
|
||||
执行已停止,请修复错误后重试。
|
||||
```
|
||||
|
||||
## 5. 技术约束
|
||||
|
||||
### 5.1 平台兼容性
|
||||
|
||||
- Windows:支持 `.bat` 可执行文件
|
||||
- Linux:支持 shell 脚本
|
||||
- 路径分隔符自动适配
|
||||
|
||||
### 5.2 性能要求
|
||||
|
||||
- 命令执行不阻塞 UI
|
||||
- 综合时间可能较长(分钟级),需要进度提示
|
||||
- 日志输出实时更新,限制缓冲区大小
|
||||
|
||||
### 5.3 安全性
|
||||
|
||||
- 工作目录限制在项目范围内
|
||||
- 许可证路径不记录到日志
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
### 6.1 功能验收
|
||||
|
||||
- [ ] 用户可以配置 Vivado 路径和 FPGA 型号
|
||||
- [ ] AI 可以通过工具调用成功执行 Vivado 综合
|
||||
- [ ] 产出文件自动导入到指定目录
|
||||
- [ ] 执行过程有清晰的进度提示
|
||||
- [ ] 报告文件可以正常打开查看
|
||||
|
||||
### 6.2 性能验收
|
||||
|
||||
- [ ] 小型项目综合时间 < 1 分钟
|
||||
- [ ] UI 响应流畅,不卡顿
|
||||
- [ ] 日志输出实时更新(延迟 < 500ms)
|
||||
|
||||
### 6.3 用户体验验收
|
||||
|
||||
- [ ] 配置界面直观易用
|
||||
- [ ] 首次使用有引导提示
|
||||
- [ ] 错误提示清晰,有解决建议
|
||||
- [ ] 导入的文件可以直接打开查看
|
||||
|
||||
## 7. 风险和依赖
|
||||
|
||||
### 7.1 风险
|
||||
|
||||
- **Vivado 版本差异**:不同版本的命令行参数可能不同
|
||||
- **许可证问题**:Vivado 需要许可证才能运行
|
||||
- **路径问题**:Windows 路径中的空格和特殊字符
|
||||
- **执行时间长**:大型项目可能需要数十分钟
|
||||
|
||||
### 7.2 依赖
|
||||
|
||||
- 用户需要自行安装 Vivado
|
||||
- 用户需要配置正确的 Vivado 路径
|
||||
- 需要设置环境变量(如 `XILINX_VIVADO`)
|
||||
- 需要有效的 Vivado 许可证
|
||||
- **需要提供 .xdc 约束文件**:
|
||||
- **管脚约束**(必需):定义信号与 FPGA 引脚的映射关系,实现阶段必须提供
|
||||
- **时序约束**(强烈推荐):定义时钟频率和时序要求,确保设计满足性能指标
|
||||
|
||||
## 8. 后续扩展
|
||||
|
||||
### 8.1 短期扩展
|
||||
|
||||
- 支持自定义 TCL 脚本模板
|
||||
- 支持批量处理多个设计
|
||||
- 支持时序约束编辑器
|
||||
|
||||
### 8.2 长期扩展
|
||||
|
||||
- 支持其他 FPGA 工具(Quartus)
|
||||
- 云端 Vivado 服务集成
|
||||
- 结果对比和版本管理
|
||||
- 性能分析和优化建议
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### A. Vivado 命令行参考
|
||||
|
||||
- 官方文档:https://docs.xilinx.com/
|
||||
- TCL 命令参考:UG835
|
||||
- 设计流程参考:UG892
|
||||
|
||||
### B. 术语表
|
||||
|
||||
- **RTL**:Register Transfer Level,寄存器传输级
|
||||
- **综合**:Synthesis,将 RTL 代码转换为门级网表
|
||||
- **实现**:Implementation,布局布线
|
||||
- **比特流**:Bitstream,FPGA 配置文件
|
||||
- **DCP**:Design Checkpoint,Vivado 设计检查点文件
|
||||
- **XDC**:Xilinx Design Constraints,约束文件
|
||||
- **LUT**:Look-Up Table,查找表(FPGA 基本逻辑单元)
|
||||
- **FF**:Flip-Flop,触发器
|
||||
166
docs/Vivado联动前后端对接文档.md
Normal file
166
docs/Vivado联动前后端对接文档.md
Normal file
@ -0,0 +1,166 @@
|
||||
# Vivado 联动前后端对接文档
|
||||
|
||||
## 1. 前端提供的工具
|
||||
|
||||
前端提供 4 个独立工具,每个工具执行一个 Vivado 命令。
|
||||
|
||||
### 1.1 createVivadoProject - 创建工程
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
{
|
||||
projectName: string; // 项目名称
|
||||
part: string; // 芯片型号(如 xc7a35tcpg236-1)
|
||||
topModule: string; // 顶层模块名
|
||||
files: string[]; // 源文件路径列表
|
||||
constraints?: string; // 约束文件路径(可选)
|
||||
mode: 'gui' | 'batch'; // gui=打开图形界面,batch=后台执行
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
{
|
||||
success: boolean; // 是否成功
|
||||
command: "create_project";
|
||||
executionTime: number; // 执行时间(毫秒)
|
||||
output: string; // 完整日志
|
||||
error?: string; // 失败原因(如果失败)
|
||||
outputFiles?: string[]; // 产出的工程文件路径
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 runVivadoSynthesis - 综合
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
{
|
||||
projectPath?: string; // 工程路径(可选)
|
||||
part: string; // 芯片型号
|
||||
topModule: string; // 顶层模块
|
||||
files?: string[]; // 源文件(如果没有工程)
|
||||
constraints?: string; // 约束文件(可选)
|
||||
mode: 'gui' | 'batch';
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
command: "synthesis";
|
||||
executionTime: number;
|
||||
output: string;
|
||||
error?: string; // 失败原因
|
||||
outputFiles?: string[]; // .dcp 文件等
|
||||
reports?: {
|
||||
resources?: string; // 资源使用摘要
|
||||
timing?: string; // 时序摘要
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 runVivadoImplementation - 实现
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 综合后的 .dcp 文件路径
|
||||
mode: 'gui' | 'batch';
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
command: "implementation";
|
||||
executionTime: number;
|
||||
output: string;
|
||||
error?: string; // 失败原因
|
||||
outputFiles?: string[]; // 实现后的 .dcp 文件等
|
||||
reports?: {
|
||||
resources?: string;
|
||||
timing?: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 runVivadoBitstream - 生成比特流
|
||||
|
||||
**参数**:
|
||||
```typescript
|
||||
{
|
||||
dcpFile: string; // 实现后的 .dcp 文件路径
|
||||
mode: 'gui' | 'batch';
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
command: "bitstream";
|
||||
executionTime: number;
|
||||
output: string;
|
||||
error?: string; // 失败原因
|
||||
outputFiles?: string[]; // .bit 文件路径
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 前端职责
|
||||
|
||||
- 接收后端工具调用
|
||||
- 生成对应的 TCL 脚本
|
||||
- 执行 Vivado 命令
|
||||
- 捕获输出日志
|
||||
- 解析报告文件(提取资源和时序摘要)
|
||||
- 返回执行结果
|
||||
|
||||
**前端不做**:
|
||||
- 不检查依赖关系
|
||||
- 不验证执行顺序
|
||||
- 不控制流程
|
||||
|
||||
## 3. 后端职责
|
||||
|
||||
- 询问用户参数(芯片型号、执行模式等)
|
||||
- 理解依赖关系(创建工程 → 综合 → 实现 → 生成比特流)
|
||||
- 按正确顺序调用工具
|
||||
- 检查每步的 `success` 字段
|
||||
- 如果失败,读取 `error` 字段并提示用户
|
||||
- 汇总结果展示给用户
|
||||
|
||||
## 4. 调用示例
|
||||
|
||||
```
|
||||
用户: 用 Vivado 做综合
|
||||
|
||||
后端:
|
||||
1. 询问芯片型号 → xc7a35tcpg236-1
|
||||
2. 询问执行模式 → batch
|
||||
|
||||
3. 调用 createVivadoProject(...)
|
||||
返回: { success: true, outputFiles: ["counter.xpr"] }
|
||||
|
||||
4. 调用 runVivadoSynthesis(...)
|
||||
返回: { success: true, outputFiles: ["counter_synth.dcp"], reports: {...} }
|
||||
|
||||
5. 展示结果给用户
|
||||
```
|
||||
|
||||
## 5. 错误处理
|
||||
|
||||
如果某步失败:
|
||||
```typescript
|
||||
{
|
||||
success: false,
|
||||
error: "ERROR: [Synth 8-439] module 'counter' not found",
|
||||
output: "详细日志..."
|
||||
}
|
||||
```
|
||||
|
||||
后端应该:
|
||||
1. 停止后续步骤
|
||||
2. 提取 `error` 字段
|
||||
3. 给用户提示和建议
|
||||
153
docs/Vivado联动功能技术设计文档.md
Normal file
153
docs/Vivado联动功能技术设计文档.md
Normal file
@ -0,0 +1,153 @@
|
||||
# Vivado 联动功能技术设计文档
|
||||
|
||||
## 1. 架构设计
|
||||
|
||||
```
|
||||
后端 AI
|
||||
↓ 调用工具
|
||||
前端 Extension (messageHandler.ts)
|
||||
↓ 调用
|
||||
VivadoRunner (utils/vivadoRunner.ts)
|
||||
↓ 生成 TCL 脚本并执行
|
||||
本地 Vivado
|
||||
```
|
||||
|
||||
## 2. 核心模块
|
||||
|
||||
### 2.1 VivadoRunner
|
||||
|
||||
**职责**:执行单个 Vivado 命令
|
||||
|
||||
**主要方法**:
|
||||
- `createProject()` - 创建工程
|
||||
- `runSynthesis()` - 执行综合
|
||||
- `runImplementation()` - 执行实现
|
||||
- `runBitstream()` - 生成比特流
|
||||
|
||||
**实现要点**:
|
||||
- 根据参数生成 TCL 脚本
|
||||
- 启动子进程执行 Vivado
|
||||
- 捕获输出日志
|
||||
- 解析报告文件
|
||||
- 返回结果
|
||||
|
||||
### 2.2 TCL 脚本生成
|
||||
|
||||
**创建工程**:
|
||||
```tcl
|
||||
create_project {projectName} {workDir} -part {part} -force
|
||||
add_files -norecurse {files}
|
||||
set_property top {topModule} [current_fileset]
|
||||
```
|
||||
|
||||
**综合**:
|
||||
```tcl
|
||||
synth_design -part {part} -top {topModule}
|
||||
report_utilization -file utilization.rpt
|
||||
write_checkpoint -force synth.dcp
|
||||
```
|
||||
|
||||
**实现**:
|
||||
```tcl
|
||||
open_checkpoint {dcpFile}
|
||||
opt_design
|
||||
place_design
|
||||
route_design
|
||||
report_timing_summary -file timing.rpt
|
||||
write_checkpoint -force impl.dcp
|
||||
```
|
||||
|
||||
**生成比特流**:
|
||||
```tcl
|
||||
open_checkpoint {dcpFile}
|
||||
write_bitstream -force output.bit
|
||||
```
|
||||
|
||||
### 2.3 MessageHandler 集成
|
||||
|
||||
```typescript
|
||||
// 处理工具调用
|
||||
async function handleToolCall(toolName: string, params: any) {
|
||||
switch(toolName) {
|
||||
case 'createVivadoProject':
|
||||
return await vivadoRunner.createProject(params);
|
||||
case 'runVivadoSynthesis':
|
||||
return await vivadoRunner.runSynthesis(params);
|
||||
case 'runVivadoImplementation':
|
||||
return await vivadoRunner.runImplementation(params);
|
||||
case 'runVivadoBitstream':
|
||||
return await vivadoRunner.runBitstream(params);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 配置管理
|
||||
|
||||
**配置文件位置**:
|
||||
- 全局:`settings.json` 中的 `ic-coder.vivado`
|
||||
- 项目:`.vscode/ic-coder-vivado.json`
|
||||
|
||||
**配置项**:
|
||||
```json
|
||||
{
|
||||
"vivado": {
|
||||
"enabled": true,
|
||||
"executablePath": "C:/Xilinx/Vivado/2023.1/bin/vivado.bat",
|
||||
"workingDir": "${workspaceFolder}/vivado_project"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── utils/
|
||||
│ ├── vivadoRunner.ts # Vivado 执行器
|
||||
│ ├── vivadoConfig.ts # 配置读取
|
||||
│ └── tclGenerator.ts # TCL 脚本生成
|
||||
└── utils/
|
||||
└── messageHandler.ts # 工具调用处理(新增部分)
|
||||
```
|
||||
|
||||
## 5. 实现要点
|
||||
|
||||
### 5.1 子进程执行
|
||||
|
||||
```typescript
|
||||
const process = spawn(vivadoPath, ['-mode', 'batch', '-source', tclPath]);
|
||||
|
||||
process.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
process.on('close', (code) => {
|
||||
resolve({ success: code === 0, output });
|
||||
});
|
||||
```
|
||||
|
||||
### 5.2 报告解析
|
||||
|
||||
从 `.rpt` 文件中提取关键信息:
|
||||
- 资源使用:LUT、FF、BRAM 数量
|
||||
- 时序信息:WNS、TNS
|
||||
|
||||
### 5.3 错误处理
|
||||
|
||||
捕获常见错误:
|
||||
- Vivado 未配置
|
||||
- 文件不存在
|
||||
- 综合/实现失败
|
||||
- 时序不满足
|
||||
|
||||
返回清晰的错误信息给后端。
|
||||
|
||||
## 6. 测试
|
||||
|
||||
**单元测试**:
|
||||
- TCL 脚本生成正确性
|
||||
- 配置读取
|
||||
|
||||
**集成测试**:
|
||||
- 完整流程测试(需要本地 Vivado)
|
||||
- 错误处理测试
|
||||
@ -200,3 +200,8 @@ export const setting = `<svg t="1768535209135" class="icon" viewBox="0 0 1024 10
|
||||
* 成功的图标svg
|
||||
*/
|
||||
export const successIconSvg = `<svg t="1771828214449" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5360" width="16" height="16"><path d="M748.864 302.528a49.024 49.024 0 0 1 68.288-1.28 46.656 46.656 0 0 1 5.952 61.44l-4.608 5.44-346.56 353.344a49.024 49.024 0 0 1-64.384 4.608l-5.504-4.864-196.8-203.264a46.72 46.72 0 0 1 1.792-66.88A49.024 49.024 0 0 1 270.08 448l5.312 4.736 162.048 167.232L748.8 302.528z" fill="#8a8a8a" p-id="5361"></path></svg>`;
|
||||
|
||||
/**
|
||||
* 任务完成的图标svg
|
||||
*/
|
||||
export const taskCompleteIconSvg = `<svg t="1773302386044" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4798" width="16" height="16"><path d="M512 42.666667C253.866667 42.666667 42.666667 253.866667 42.666667 512s211.2 469.333333 469.333333 469.333333 469.333333-211.2 469.333333-469.333333S770.133333 42.666667 512 42.666667z m221.866667 377.6L488.533333 663.466667c-8.533333 8.533333-19.2 12.8-29.866666 12.8s-21.333333-4.266667-29.866667-12.8l-138.666667-138.666667c-17.066667-17.066667-17.066667-42.666667 0-59.733333 17.066667-17.066667 42.666667-17.066667 59.733334 0l108.8 108.8 215.466666-215.466667c17.066667-17.066667 42.666667-17.066667 59.733334 0 17.066667 17.066667 17.066667 44.8 0 61.866667z" fill="#1afa29" p-id="4799" data-spm-anchor-id="a313x.search_index.0.i0.123d3a812ZEn1Z" class=""></path></svg>`;
|
||||
|
||||
@ -31,7 +31,11 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
if (!editor) return;
|
||||
|
||||
if (!editor.selection.isEmpty) {
|
||||
const range = new vscode.Range(editor.selection.end, editor.selection.end);
|
||||
// 找到选区末尾所在的行,并将提示放在该行的末尾
|
||||
const { anchor, active } = editor.selection;
|
||||
const endPos = anchor.isAfter(active) ? anchor : active;
|
||||
const lineEndPos = editor.document.lineAt(endPos.line).range.end;
|
||||
const range = new vscode.Range(lineEndPos, lineEndPos);
|
||||
const decoration = { range };
|
||||
editor.setDecorations(decorationType, [decoration]);
|
||||
} else {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
66
src/panels/helpers/authHelper.ts
Normal file
66
src/panels/helpers/authHelper.ts
Normal file
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 认证辅助模块
|
||||
* 功能:处理用户登录状态检查和 token 验证
|
||||
* 依赖:vscode, jwtUtils
|
||||
* 使用场景:面板初始化时验证用户登录状态
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
import { isTokenExpired } from "../../utils/jwtUtils";
|
||||
|
||||
export async function checkAuthAndPromptLogin(
|
||||
context: vscode.ExtensionContext,
|
||||
): Promise<boolean> {
|
||||
let token: string | undefined;
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
token = session?.accessToken;
|
||||
} catch (error) {
|
||||
console.warn("[AuthHelper] 获取 session 失败:", error);
|
||||
}
|
||||
|
||||
if (token && isTokenExpired(token)) {
|
||||
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", { forceReauth: true });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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 false;
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
104
src/panels/helpers/contextHelper.ts
Normal file
104
src/panels/helpers/contextHelper.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 上下文管理模块
|
||||
* 功能:处理文件、文件夹、图片、文档上下文添加
|
||||
* 依赖:vscode, fs, path
|
||||
* 使用场景:用户添加上下文项时
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
|
||||
export async function handleAddContextFile(panel: vscode.WebviewPanel) {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await vscode.workspace.findFiles(
|
||||
"**/*",
|
||||
"**/node_modules/**",
|
||||
);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "showWorkspaceFileList",
|
||||
files: files.map((uri) => ({
|
||||
path: uri.fsPath,
|
||||
relativePath: vscode.workspace.asRelativePath(uri),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleAddContextFolder(panel: vscode.WebviewPanel) {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (!workspaceFolder) {
|
||||
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||
return;
|
||||
}
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const folders: Array<{ path: string; relativePath: string }> = [];
|
||||
|
||||
function scanFolders(dir: string, baseDir: string) {
|
||||
try {
|
||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const item of items) {
|
||||
if (
|
||||
item.isDirectory() &&
|
||||
item.name !== "node_modules" &&
|
||||
!item.name.startsWith(".")
|
||||
) {
|
||||
const fullPath = path.join(dir, item.name);
|
||||
const relativePath = path.relative(baseDir, fullPath);
|
||||
folders.push({ path: fullPath, relativePath });
|
||||
scanFolders(fullPath, baseDir);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("扫描文件夹失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
scanFolders(workspaceFolder.uri.fsPath, workspaceFolder.uri.fsPath);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "showWorkspaceFolderList",
|
||||
folders: folders,
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleAddContextImage(panel: vscode.WebviewPanel) {
|
||||
const imageUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: true,
|
||||
openLabel: "选择图片",
|
||||
filters: {
|
||||
图片文件: ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
||||
},
|
||||
});
|
||||
if (imageUris && imageUris.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "contextImagesSelected",
|
||||
images: imageUris.map((uri) => uri.fsPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleAddContextDocument(panel: vscode.WebviewPanel) {
|
||||
const docUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: true,
|
||||
openLabel: "选择文档",
|
||||
filters: {
|
||||
文档文件: ["pdf", "doc", "docx", "txt", "md"],
|
||||
所有文件: ["*"],
|
||||
},
|
||||
});
|
||||
if (docUris && docUris.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "contextDocumentsSelected",
|
||||
documents: docUris.map((uri) => uri.fsPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
224
src/panels/helpers/conversationHelper.ts
Normal file
224
src/panels/helpers/conversationHelper.ts
Normal file
@ -0,0 +1,224 @@
|
||||
/**
|
||||
* 会话历史管理模块
|
||||
* 功能:加载和选择会话历史
|
||||
* 依赖:vscode, chatHistoryManager, messageHandler
|
||||
* 使用场景:会话历史列表和切换
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
import { ChatHistoryManager } from "../../utils/chatHistoryManager";
|
||||
import { MessageType } from "../../types/chatHistory";
|
||||
import { setLastTaskId } from "../../utils/messageHandler";
|
||||
|
||||
export async function loadConversationHistory(
|
||||
panel: vscode.WebviewPanel,
|
||||
offset: number = 0,
|
||||
limit: number = 10,
|
||||
) {
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
|
||||
if (!workspacePath) {
|
||||
panel.webview.postMessage({
|
||||
command: "conversationHistory",
|
||||
items: [],
|
||||
total: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await historyManager.getConversationHistoryList(
|
||||
workspacePath,
|
||||
offset,
|
||||
limit,
|
||||
);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "conversationHistory",
|
||||
items: result.items,
|
||||
total: result.total,
|
||||
hasMore: result.hasMore,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载会话历史失败:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "conversationHistory",
|
||||
items: [],
|
||||
total: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectConversation(
|
||||
panel: vscode.WebviewPanel,
|
||||
taskId: string,
|
||||
extensionPath: string,
|
||||
) {
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
|
||||
if (!workspacePath) {
|
||||
vscode.window.showErrorMessage("没有打开的工作区");
|
||||
return;
|
||||
}
|
||||
|
||||
const taskSession = await historyManager.loadTaskSession(
|
||||
workspacePath,
|
||||
taskId,
|
||||
);
|
||||
|
||||
if (!taskSession) {
|
||||
vscode.window.showErrorMessage(
|
||||
`加载任务 ${taskId} 失败: 任务不存在或数据损坏`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const switched = await historyManager.switchTask(workspacePath, taskId);
|
||||
if (!switched) {
|
||||
vscode.window.showErrorMessage(`切换到任务 ${taskId} 失败`);
|
||||
return;
|
||||
}
|
||||
|
||||
setLastTaskId(taskId);
|
||||
|
||||
const panelId = (panel as any).__uniqueId;
|
||||
historyManager.setPanelTask(panelId, taskId, workspacePath);
|
||||
|
||||
panel.webview.postMessage({ command: "clearChat" });
|
||||
|
||||
const segments: any[] = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < taskSession.messages.length) {
|
||||
const message = taskSession.messages[i];
|
||||
|
||||
if (message.type === MessageType.USER) {
|
||||
if (segments.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveSegments",
|
||||
segments: [...segments],
|
||||
});
|
||||
segments.length = 0;
|
||||
}
|
||||
|
||||
const textContent = message.contents?.find((c) => c.type === "TEXT");
|
||||
if (textContent && "text" in textContent) {
|
||||
panel.webview.postMessage({
|
||||
command: "addUserMessage",
|
||||
text: textContent.text,
|
||||
});
|
||||
}
|
||||
i++;
|
||||
} else if (message.type === MessageType.AI) {
|
||||
if (message.segments && message.segments.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveSegments",
|
||||
segments: message.segments,
|
||||
});
|
||||
i++;
|
||||
} else {
|
||||
if (message.text) {
|
||||
segments.push({ type: "text", content: message.text });
|
||||
}
|
||||
|
||||
if (
|
||||
message.toolExecutionRequests &&
|
||||
message.toolExecutionRequests.length > 0
|
||||
) {
|
||||
for (const toolReq of message.toolExecutionRequests) {
|
||||
let toolResult = "";
|
||||
if (i + 1 < taskSession.messages.length) {
|
||||
const nextMsg = taskSession.messages[i + 1];
|
||||
if (
|
||||
nextMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
|
||||
nextMsg.id === toolReq.id
|
||||
) {
|
||||
toolResult = nextMsg.text;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
segments.push({
|
||||
type: "tool",
|
||||
toolName: toolReq.name,
|
||||
askId: toolReq.id,
|
||||
toolResult: toolResult,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
|
||||
while (i < taskSession.messages.length) {
|
||||
const nextMsg = taskSession.messages[i];
|
||||
if (nextMsg.type === MessageType.USER) {
|
||||
break;
|
||||
}
|
||||
if (nextMsg.type === MessageType.AI) {
|
||||
if (nextMsg.segments && nextMsg.segments.length > 0) {
|
||||
break;
|
||||
}
|
||||
if (nextMsg.text) {
|
||||
segments.push({ type: "text", content: nextMsg.text });
|
||||
}
|
||||
if (
|
||||
nextMsg.toolExecutionRequests &&
|
||||
nextMsg.toolExecutionRequests.length > 0
|
||||
) {
|
||||
for (const toolReq of nextMsg.toolExecutionRequests) {
|
||||
let toolResult = "";
|
||||
if (i + 1 < taskSession.messages.length) {
|
||||
const resultMsg = taskSession.messages[i + 1];
|
||||
if (
|
||||
resultMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
|
||||
resultMsg.id === toolReq.id
|
||||
) {
|
||||
toolResult = resultMsg.text;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
segments.push({
|
||||
type: "tool",
|
||||
toolName: toolReq.name,
|
||||
askId: toolReq.id,
|
||||
toolResult: toolResult,
|
||||
});
|
||||
}
|
||||
}
|
||||
i++;
|
||||
} else if (nextMsg.type === MessageType.TOOL_EXECUTION_RESULT) {
|
||||
i++;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (segments.length > 0) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveSegments",
|
||||
segments: segments,
|
||||
});
|
||||
}
|
||||
|
||||
// 发送任务完成消息(历史记录)
|
||||
panel.webview.postMessage({
|
||||
command: "taskCompleteHistory",
|
||||
});
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`已加载会话: ${taskSession.meta.taskName}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("选择会话失败:", error);
|
||||
vscode.window.showErrorMessage(`加载会话失败: ${error}`);
|
||||
}
|
||||
}
|
||||
78
src/panels/helpers/fileHelper.ts
Normal file
78
src/panels/helpers/fileHelper.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 文件操作辅助模块
|
||||
* 功能:处理文件打开、选择等操作
|
||||
* 依赖:vscode, fs, path
|
||||
* 使用场景:打开文件、跳转到代码位置
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
|
||||
export async function openFile(filePath: string) {
|
||||
const path = require("path");
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const fullPath =
|
||||
path.isAbsolute(filePath) || !workspaceFolder
|
||||
? filePath
|
||||
: vscode.Uri.joinPath(workspaceFolder.uri, filePath).fsPath;
|
||||
const doc = await vscode.workspace.openTextDocument(fullPath);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
}
|
||||
|
||||
export async function openFileWithSelection(
|
||||
filePath: string,
|
||||
startLine: number,
|
||||
endLine: number,
|
||||
) {
|
||||
const path = require("path");
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const fullPath =
|
||||
path.isAbsolute(filePath) || !workspaceFolder
|
||||
? filePath
|
||||
: vscode.Uri.joinPath(workspaceFolder.uri, filePath).fsPath;
|
||||
const doc = await vscode.workspace.openTextDocument(fullPath);
|
||||
const editor = await vscode.window.showTextDocument(doc);
|
||||
const start = new vscode.Position(startLine - 1, 0);
|
||||
const end = new vscode.Position(
|
||||
endLine - 1,
|
||||
doc.lineAt(endLine - 1).text.length,
|
||||
);
|
||||
editor.selection = new vscode.Selection(start, end);
|
||||
editor.revealRange(new vscode.Range(start, end));
|
||||
}
|
||||
|
||||
export async function openFilePathTag(
|
||||
filePath: string,
|
||||
startLine?: number,
|
||||
endLine?: number,
|
||||
) {
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
|
||||
let fullPath = filePath;
|
||||
|
||||
if (!path.isAbsolute(filePath) && workspaceFolder) {
|
||||
const candidatePath = vscode.Uri.joinPath(
|
||||
workspaceFolder.uri,
|
||||
filePath,
|
||||
).fsPath;
|
||||
if (fs.existsSync(candidatePath)) {
|
||||
fullPath = candidatePath;
|
||||
} else {
|
||||
const fileName = path.basename(filePath);
|
||||
const files = await vscode.workspace.findFiles(
|
||||
`**/${fileName}`,
|
||||
"**/node_modules/**",
|
||||
1,
|
||||
);
|
||||
if (files.length > 0) {
|
||||
fullPath = files[0].fsPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startLine && endLine) {
|
||||
await openFileWithSelection(fullPath, startLine, endLine);
|
||||
} else {
|
||||
await openFile(fullPath);
|
||||
}
|
||||
}
|
||||
396
src/panels/helpers/messageRouter.ts
Normal file
396
src/panels/helpers/messageRouter.ts
Normal file
@ -0,0 +1,396 @@
|
||||
/**
|
||||
* 消息路由处理模块
|
||||
* 功能:处理 webview 消息的路由分发
|
||||
* 依赖:各个 helper 模块和 messageHandler
|
||||
* 使用场景:webview 消息接收时
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
import {
|
||||
handleUserMessage,
|
||||
insertCodeToEditor,
|
||||
handleReadFile,
|
||||
handleUpdateFile,
|
||||
handleRenameFile,
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog,
|
||||
handleOptimizePrompt,
|
||||
handlePlanAction,
|
||||
getCurrentTaskId,
|
||||
handleAcceptChange,
|
||||
handleRejectChange,
|
||||
handleOpenFileDiff,
|
||||
startChangeSession,
|
||||
} from "../../utils/messageHandler";
|
||||
import { compactDialog } from "../../services/apiClient";
|
||||
import { ChatHistoryManager } from "../../utils/chatHistoryManager";
|
||||
import { getCachedUserInfo } from "../../services/userService";
|
||||
import { loadConversationHistory, selectConversation } from "./conversationHelper";
|
||||
import { getVCDFileInfo } from "./vcdHelper";
|
||||
import {
|
||||
handleAddContextFile,
|
||||
handleAddContextFolder,
|
||||
handleAddContextImage,
|
||||
handleAddContextDocument,
|
||||
} from "./contextHelper";
|
||||
import { openFile, openFileWithSelection, openFilePathTag } from "./fileHelper";
|
||||
|
||||
export async function handleWebviewMessage(
|
||||
message: any,
|
||||
panel: vscode.WebviewPanel,
|
||||
context: vscode.ExtensionContext,
|
||||
) {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const panelId = (panel as any).__uniqueId;
|
||||
|
||||
switch (message.command) {
|
||||
case "sendMessage":
|
||||
if (!historyManager.getPanelTask(panelId)) {
|
||||
const workspacePath =
|
||||
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (workspacePath) {
|
||||
try {
|
||||
const taskMeta = await historyManager.createTask(
|
||||
workspacePath,
|
||||
"新对话",
|
||||
);
|
||||
historyManager.setPanelTask(
|
||||
panelId,
|
||||
taskMeta.taskId,
|
||||
workspacePath,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("创建任务失败:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
historyManager.switchToPanelTask(panelId);
|
||||
const sessionId = `session_${panelId}_${Date.now()}`;
|
||||
startChangeSession(sessionId);
|
||||
panel.webview.postMessage({ type: "showProgress" });
|
||||
|
||||
handleUserMessage(
|
||||
panel,
|
||||
message.text,
|
||||
context.extensionPath,
|
||||
message.mode,
|
||||
message.model,
|
||||
message.contextItems,
|
||||
);
|
||||
break;
|
||||
|
||||
case "readFile":
|
||||
handleReadFile(panel, message.filePath);
|
||||
break;
|
||||
|
||||
case "updateFile":
|
||||
handleUpdateFile(panel, message.filePath, message.content);
|
||||
break;
|
||||
|
||||
case "renameFile":
|
||||
handleRenameFile(panel, message.oldPath, message.newPath);
|
||||
break;
|
||||
|
||||
case "replaceInFile":
|
||||
handleReplaceInFile(
|
||||
panel,
|
||||
message.filePath,
|
||||
message.searchText,
|
||||
message.replaceText,
|
||||
);
|
||||
break;
|
||||
|
||||
case "insertCode":
|
||||
insertCodeToEditor(message.code);
|
||||
break;
|
||||
|
||||
case "showInfo":
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
|
||||
case "openWaveformViewer":
|
||||
if (message.vcdFilePath) {
|
||||
vscode.commands.executeCommand(
|
||||
"ic-coder.openVCDViewer",
|
||||
message.vcdFilePath,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "getVCDInfo":
|
||||
if (message.vcdFilePath && message.containerId) {
|
||||
getVCDFileInfo(panel, message.vcdFilePath, message.containerId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "createNewConversation":
|
||||
const { showICHelperPanel } = require("../ICHelperPanel");
|
||||
showICHelperPanel(context, panel.viewColumn);
|
||||
break;
|
||||
|
||||
case "loadConversationHistory":
|
||||
loadConversationHistory(
|
||||
panel,
|
||||
message.offset || 0,
|
||||
message.limit || 10,
|
||||
);
|
||||
break;
|
||||
|
||||
case "selectConversation":
|
||||
if (message.conversationId) {
|
||||
selectConversation(panel, message.conversationId, context.extensionPath);
|
||||
}
|
||||
break;
|
||||
|
||||
case "submitAnswer":
|
||||
void handleUserAnswer(
|
||||
message.askId,
|
||||
message.selected,
|
||||
message.customInput,
|
||||
message.answers,
|
||||
);
|
||||
break;
|
||||
|
||||
case "abortDialog":
|
||||
void abortCurrentDialog();
|
||||
break;
|
||||
|
||||
case "compressConversation":
|
||||
{
|
||||
const taskId = getCurrentTaskId();
|
||||
if (taskId) {
|
||||
compactDialog(taskId)
|
||||
.then((result) => {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: result.success
|
||||
? "✅ 会话压缩完成"
|
||||
: `❌ 压缩失败: ${result.error || "未知错误"}`,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 压缩失败: ${err.message || "网络错误"}`,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: "❌ 没有活跃的会话",
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "optimizePrompt":
|
||||
if (typeof message.prompt === "string") {
|
||||
void handleOptimizePrompt(panel, message.prompt);
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "optimizeResult",
|
||||
success: false,
|
||||
error: "提示词为空或格式错误",
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "logout":
|
||||
vscode.commands.executeCommand("ic-coder.logout");
|
||||
break;
|
||||
|
||||
case "openFile":
|
||||
if (message.filePath) {
|
||||
await openFile(message.filePath);
|
||||
}
|
||||
break;
|
||||
|
||||
case "openFileWithSelection":
|
||||
if (message.filePath) {
|
||||
await openFileWithSelection(
|
||||
message.filePath,
|
||||
message.startLine,
|
||||
message.endLine,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "openFilePathTag":
|
||||
if (message.filePath) {
|
||||
await openFilePathTag(
|
||||
message.filePath,
|
||||
message.startLine,
|
||||
message.endLine,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "acceptChange":
|
||||
if (message.changeId) {
|
||||
await handleAcceptChange(panel, message.changeId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "rejectChange":
|
||||
if (message.changeId) {
|
||||
await handleRejectChange(panel, message.changeId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "openFileDiff":
|
||||
if (message.changeId) {
|
||||
await handleOpenFileDiff(panel, message.changeId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "checkInvitationCode":
|
||||
{
|
||||
const userInfo = getCachedUserInfo();
|
||||
if (userInfo?.isPluginTrial === true) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "checkWelcomeModal":
|
||||
{
|
||||
const userInfo = getCachedUserInfo();
|
||||
if (userInfo?.isPluginTrial === true) {
|
||||
if (userInfo.pluginTrialExpiresAt === undefined) {
|
||||
break;
|
||||
}
|
||||
if (userInfo.pluginTrialExpiresAt !== null) {
|
||||
const now = Date.now();
|
||||
const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
||||
if (isExpired) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
panel.webview.postMessage({ command: "showWelcomeModal" });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "checkTrialExpiration":
|
||||
{
|
||||
const { TrialExpirationService } = require("../../services/trialExpirationService");
|
||||
const trialService = new TrialExpirationService(context, panel);
|
||||
await trialService.checkExpiration();
|
||||
}
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "openICCoder":
|
||||
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||
break;
|
||||
|
||||
case "openTutorial":
|
||||
vscode.env.openExternal(
|
||||
vscode.Uri.parse(
|
||||
"https://www.iccoder.com/guides/quick-start/first-task-plugin",
|
||||
),
|
||||
);
|
||||
break;
|
||||
|
||||
case "openUserManual":
|
||||
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||
break;
|
||||
|
||||
case "openUserFeedback":
|
||||
panel.webview.postMessage({ command: "showFeedbackQRCode" });
|
||||
break;
|
||||
|
||||
case "planAction":
|
||||
if (message.action === "confirm") {
|
||||
panel.webview.postMessage({ command: "switchMode", mode: "agent" });
|
||||
} else if (message.action === "modify" || message.action === "cancel") {
|
||||
void handlePlanAction(
|
||||
panel,
|
||||
message.action,
|
||||
message.planTitle || "",
|
||||
context.extensionPath,
|
||||
message.model,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "addContextFile":
|
||||
await handleAddContextFile(panel);
|
||||
break;
|
||||
|
||||
case "addContextFolder":
|
||||
await handleAddContextFolder(panel);
|
||||
break;
|
||||
|
||||
case "addContextImage":
|
||||
await handleAddContextImage(panel);
|
||||
break;
|
||||
|
||||
case "addContextDocument":
|
||||
await handleAddContextDocument(panel);
|
||||
break;
|
||||
|
||||
case "checkWorkspace":
|
||||
const hasWorkspace = !!(
|
||||
vscode.workspace.workspaceFolders &&
|
||||
vscode.workspace.workspaceFolders.length > 0
|
||||
);
|
||||
if (!hasWorkspace) {
|
||||
vscode.window
|
||||
.showWarningMessage(
|
||||
"请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊",
|
||||
"打开文件夹",
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "打开文件夹") {
|
||||
vscode.commands.executeCommand("vscode.openFolder");
|
||||
}
|
||||
});
|
||||
}
|
||||
panel.webview.postMessage({
|
||||
command: "workspaceStatus",
|
||||
hasWorkspace: hasWorkspace,
|
||||
});
|
||||
break;
|
||||
|
||||
case "openExternalUrl":
|
||||
if (message.url) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
116
src/panels/helpers/userInfoHelper.ts
Normal file
116
src/panels/helpers/userInfoHelper.ts
Normal file
@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 用户信息辅助模块
|
||||
* 功能:管理用户信息的获取、更新和发送
|
||||
* 依赖:vscode, userService, creditsService
|
||||
* 使用场景:面板初始化和余额更新时
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
import { getCachedUserInfo } from "../../services/userService";
|
||||
import { setBalanceUpdateCallback } from "../../services/creditsService";
|
||||
|
||||
export 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,
|
||||
"dist",
|
||||
"assets",
|
||||
"titleIcon",
|
||||
iconFile,
|
||||
),
|
||||
);
|
||||
|
||||
return iconUri.toString();
|
||||
}
|
||||
|
||||
export async function sendUserInfoToWebview(
|
||||
panel: vscode.WebviewPanel,
|
||||
context: vscode.ExtensionContext,
|
||||
) {
|
||||
try {
|
||||
let userInfo = getCachedUserInfo();
|
||||
|
||||
if (userInfo) {
|
||||
console.log("[UserInfoHelper] 使用缓存的用户信息:", userInfo);
|
||||
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: userInfo.credits,
|
||||
membership: userInfo.membership,
|
||||
},
|
||||
tierIconUrl: tierIconUrl,
|
||||
});
|
||||
} else {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
if (session) {
|
||||
panel.webview.postMessage({
|
||||
command: "updateUserInfo",
|
||||
userInfo: {
|
||||
userId: session.account.id,
|
||||
nickname: session.account.label,
|
||||
username: session.account.label,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[UserInfoHelper] 获取用户信息失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export function setupBalanceUpdateCallback(
|
||||
panel: vscode.WebviewPanel,
|
||||
context: vscode.ExtensionContext,
|
||||
) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
158
src/panels/helpers/vcdHelper.ts
Normal file
158
src/panels/helpers/vcdHelper.ts
Normal file
@ -0,0 +1,158 @@
|
||||
/**
|
||||
* VCD 文件处理模块
|
||||
* 功能:VCD 文件信息获取和信号解析
|
||||
* 依赖:vscode, fs
|
||||
* 使用场景:波形查看器相关功能
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
|
||||
export async function getVCDFileInfo(
|
||||
panel: vscode.WebviewPanel,
|
||||
vcdFilePath: string,
|
||||
containerId: string,
|
||||
) {
|
||||
try {
|
||||
const fs = require("fs");
|
||||
|
||||
if (!fs.existsSync(vcdFilePath)) {
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: "N/A",
|
||||
timeRange: "N/A",
|
||||
fileSize: "N/A",
|
||||
error: "文件不存在",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = fs.statSync(vcdFilePath);
|
||||
const fileSizeKB = stats.size / 1024;
|
||||
const fileSize =
|
||||
fileSizeKB < 1024
|
||||
? `${fileSizeKB.toFixed(2)} KB`
|
||||
: `${(fileSizeKB / 1024).toFixed(2)} MB`;
|
||||
|
||||
const content = fs.readFileSync(vcdFilePath, "utf-8");
|
||||
const varMatches = content.match(/\$var/g);
|
||||
const signalCount = varMatches ? varMatches.length : 0;
|
||||
|
||||
let timeRange = "N/A";
|
||||
const timeMatch = content.match(/#(\d+)/g);
|
||||
if (timeMatch && timeMatch.length > 0) {
|
||||
const times = timeMatch.map((t: string) => parseInt(t.substring(1)));
|
||||
const minTime = Math.min(...times);
|
||||
const maxTime = Math.max(...times);
|
||||
timeRange = `${minTime} - ${maxTime}`;
|
||||
}
|
||||
|
||||
const signals = parseVCDSignals(content, 3);
|
||||
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: signalCount.toString(),
|
||||
timeRange: timeRange,
|
||||
fileSize: fileSize,
|
||||
signals: signals,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("获取 VCD 文件信息失败:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "vcdInfo",
|
||||
containerId: containerId,
|
||||
vcdInfo: {
|
||||
signalCount: "N/A",
|
||||
timeRange: "N/A",
|
||||
fileSize: "N/A",
|
||||
error: error instanceof Error ? error.message : "未知错误",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function parseVCDSignals(content: string, maxSignals: number = 3) {
|
||||
const signals: Array<{
|
||||
name: string;
|
||||
identifier: string;
|
||||
width: number;
|
||||
values: Array<{ time: number; value: string }>;
|
||||
}> = [];
|
||||
|
||||
try {
|
||||
const varRegex = /\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+([^\$]+?)\s+\$end/g;
|
||||
let match;
|
||||
const signalDefs: Array<{
|
||||
name: string;
|
||||
identifier: string;
|
||||
width: number;
|
||||
}> = [];
|
||||
|
||||
while (
|
||||
(match = varRegex.exec(content)) !== null &&
|
||||
signalDefs.length < maxSignals
|
||||
) {
|
||||
const width = parseInt(match[2]);
|
||||
const identifier = match[3];
|
||||
const name = match[4].trim();
|
||||
signalDefs.push({ name, identifier, width });
|
||||
}
|
||||
|
||||
const dumpvarsIndex = content.indexOf("$dumpvars");
|
||||
if (dumpvarsIndex === -1) {
|
||||
return signals;
|
||||
}
|
||||
|
||||
const dataSection = content.substring(dumpvarsIndex);
|
||||
|
||||
for (const signalDef of signalDefs) {
|
||||
const values: Array<{ time: number; value: string }> = [];
|
||||
let currentTime = 0;
|
||||
const lines = dataSection.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
if (trimmedLine.startsWith("#")) {
|
||||
currentTime = parseInt(trimmedLine.substring(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (signalDef.width === 1) {
|
||||
const singleBitMatch = trimmedLine.match(
|
||||
new RegExp(`^([01xz])${signalDef.identifier}$`),
|
||||
);
|
||||
if (singleBitMatch) {
|
||||
values.push({ time: currentTime, value: singleBitMatch[1] });
|
||||
}
|
||||
} else {
|
||||
const multiBitMatch = trimmedLine.match(
|
||||
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`),
|
||||
);
|
||||
if (multiBitMatch) {
|
||||
values.push({ time: currentTime, value: multiBitMatch[1] });
|
||||
}
|
||||
}
|
||||
|
||||
if (values.length >= 50) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
signals.push({
|
||||
name: signalDef.name,
|
||||
identifier: signalDef.identifier,
|
||||
width: signalDef.width,
|
||||
values: values,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("解析 VCD 信号数据失败:", error);
|
||||
}
|
||||
|
||||
return signals;
|
||||
}
|
||||
@ -291,7 +291,7 @@ async function executeSyntaxCheck(
|
||||
// 检查 iverilog 是否可用
|
||||
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||||
if (!iverilogCheck.available) {
|
||||
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
|
||||
throw new Error(`IC Coder编译器不可用: ${iverilogCheck.message}`);
|
||||
}
|
||||
|
||||
// 创建临时文件
|
||||
@ -372,7 +372,7 @@ async function executeIverilog(
|
||||
// 检查 iverilog 是否可用
|
||||
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||||
if (!iverilogCheck.available) {
|
||||
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
|
||||
throw new Error(`IC Coder编译器不可用: ${iverilogCheck.message}`);
|
||||
}
|
||||
|
||||
// 获取工作目录
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -209,12 +209,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 +276,7 @@ async function getVvpPath(extensionPath: string): Promise<string> {
|
||||
*/
|
||||
export async function generateVCD(
|
||||
projectPath: string,
|
||||
extensionPath: string
|
||||
extensionPath: string,
|
||||
): Promise<VCDGenerationResult> {
|
||||
try {
|
||||
// 1. 检查项目完整性
|
||||
@ -302,12 +320,27 @@ export async function generateVCD(
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: `iverilog 编译失败:\n${error.message}`,
|
||||
message: `IC Coder编译器编译失败:\n${error.message}`,
|
||||
stderr: error.stderr,
|
||||
stdout: error.stdout,
|
||||
};
|
||||
}
|
||||
|
||||
// 6.5. 删除 shebang 行(修复 Windows 上的 vvp 解析错误)
|
||||
try {
|
||||
const fs = require("fs");
|
||||
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 行");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("删除 shebang 失败,继续执行:", error);
|
||||
}
|
||||
|
||||
// 7. 执行仿真生成 VCD
|
||||
const simArgs = [outputFile];
|
||||
console.log("执行仿真命令:", vvpPath, simArgs.join(" "));
|
||||
@ -331,13 +364,17 @@ export async function generateVCD(
|
||||
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'))
|
||||
.filter(
|
||||
([fileName, fileType]) =>
|
||||
fileType === vscode.FileType.File && fileName.endsWith(".vcd"),
|
||||
)
|
||||
.map(([fileName]) => fileName);
|
||||
|
||||
if (vcdFiles.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: "VCD 文件未生成。请确保 testbench 中包含 $dumpfile 和 $dumpvars 语句。",
|
||||
message:
|
||||
"VCD 文件未生成。请确保 testbench 中包含 $dumpfile 和 $dumpvars 语句。",
|
||||
stdout: simResult.stdout,
|
||||
};
|
||||
}
|
||||
@ -373,7 +410,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 +422,7 @@ export async function checkIverilogAvailable(
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
message: `iverilog 不可用。未找到文件: ${iverilogPath}`,
|
||||
message: `IC Coder编译器不可用。未找到文件: ${iverilogPath}`,
|
||||
};
|
||||
}
|
||||
|
||||
@ -404,12 +441,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 +455,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 +481,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 +507,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 +518,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 +526,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 +542,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 +558,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);
|
||||
@ -554,27 +597,34 @@ export async function generateMultiVCD(
|
||||
// 编译(带宏定义)
|
||||
const compileArgs = [
|
||||
`-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 +637,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 : "未知错误"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
checkVerilogProject,
|
||||
checkIverilogAvailable,
|
||||
} from "./iverilogRunner";
|
||||
import { createVivadoProject, runVivadoSynthesis } from "./vivadoRunner";
|
||||
import { ChatHistoryManager } from "./chatHistoryManager";
|
||||
import { dialogManager, DialogSession } from "../services/dialogService";
|
||||
import { userInteractionManager } from "../services/userInteraction";
|
||||
@ -41,7 +42,11 @@ let currentSession: DialogSession | null = null;
|
||||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||
let lastTaskId: string | null = null;
|
||||
|
||||
async function trackFileChange(filePath: string, oldContent: string, newContent: string): Promise<void> {
|
||||
async function trackFileChange(
|
||||
filePath: string,
|
||||
oldContent: string,
|
||||
newContent: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
changeTracker.trackChange(filePath, oldContent, newContent);
|
||||
} catch (error) {
|
||||
@ -58,7 +63,7 @@ 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);
|
||||
|
||||
@ -68,7 +73,9 @@ export async function handleUserMessage(
|
||||
// 从 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,20 +85,20 @@ 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 === '立即登录') {
|
||||
if (action === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
@ -110,24 +117,24 @@ export async function handleUserMessage(
|
||||
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 === '立即登录') {
|
||||
if (action === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login", {
|
||||
forceReauth: true,
|
||||
});
|
||||
@ -190,11 +197,11 @@ export async function handleUserMessage(
|
||||
// 显示错误提示
|
||||
const selection = await vscode.window.showWarningMessage(
|
||||
balanceCheck.message || "资源点余额不足",
|
||||
"去充值"
|
||||
"去充值",
|
||||
);
|
||||
if (selection === "去充值") {
|
||||
vscode.env.openExternal(
|
||||
vscode.Uri.parse("https://iccoder.com/memberCenter")
|
||||
vscode.Uri.parse("https://iccoder.com/memberCenter"),
|
||||
);
|
||||
}
|
||||
// 恢复输入状态
|
||||
@ -216,14 +223,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",
|
||||
});
|
||||
// 恢复输入状态
|
||||
@ -254,7 +261,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();
|
||||
|
||||
@ -262,7 +269,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}`;
|
||||
}
|
||||
|
||||
@ -273,7 +280,7 @@ async function handleUserMessageWithBackend(
|
||||
// 创建会话(dialogManager 会自动处理旧会话的中止)
|
||||
currentSession = dialogManager.createSession(
|
||||
extensionPath,
|
||||
taskIdToUse || undefined
|
||||
taskIdToUse || undefined,
|
||||
);
|
||||
// 保存 taskId 用于后续操作(如压缩)
|
||||
lastTaskId = currentSession.getTaskId();
|
||||
@ -281,7 +288,7 @@ async function handleUserMessageWithBackend(
|
||||
"[MessageHandler] 创建会话: taskId=",
|
||||
lastTaskId,
|
||||
"来源=",
|
||||
taskIdToUse ? "historyManager" : "新生成"
|
||||
taskIdToUse ? "historyManager" : "新生成",
|
||||
);
|
||||
|
||||
// 显示状态栏
|
||||
@ -300,10 +307,18 @@ 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,
|
||||
});
|
||||
},
|
||||
|
||||
@ -324,7 +339,10 @@ async function handleUserMessageWithBackend(
|
||||
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||
},
|
||||
|
||||
onQuestion: (askId: string, questions: import("../types/api").QuestionItem[]) => {
|
||||
onQuestion: (
|
||||
askId: string,
|
||||
questions: import("../types/api").QuestionItem[],
|
||||
) => {
|
||||
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
@ -376,21 +394,29 @@ async function handleUserMessageWithBackend(
|
||||
isComplete: true,
|
||||
});
|
||||
|
||||
// 发送任务完成消息
|
||||
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();
|
||||
@ -458,7 +484,7 @@ async function handleUserMessageWithBackend(
|
||||
},
|
||||
},
|
||||
mode,
|
||||
serviceTier // 传递服务等级
|
||||
serviceTier, // 传递服务等级
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -470,7 +496,7 @@ export async function handleUserAnswer(
|
||||
askId: string,
|
||||
selected?: string[],
|
||||
customInput?: string,
|
||||
answers?: { [questionIndex: string]: string[] }
|
||||
answers?: { [questionIndex: string]: string[] },
|
||||
): Promise<void> {
|
||||
if (currentSession) {
|
||||
await currentSession.submitAnswer(askId, selected, customInput, answers);
|
||||
@ -540,7 +566,7 @@ export async function handlePlanAction(
|
||||
action: string,
|
||||
planTitle: string,
|
||||
extensionPath: string,
|
||||
serviceTier?: ServiceTier
|
||||
serviceTier?: ServiceTier,
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
"[handlePlanAction] action:",
|
||||
@ -548,7 +574,7 @@ export async function handlePlanAction(
|
||||
"planTitle:",
|
||||
planTitle,
|
||||
"serviceTier:",
|
||||
serviceTier
|
||||
serviceTier,
|
||||
);
|
||||
|
||||
switch (action) {
|
||||
@ -564,7 +590,7 @@ export async function handlePlanAction(
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
extensionPath,
|
||||
"agent",
|
||||
serviceTier
|
||||
serviceTier,
|
||||
);
|
||||
break;
|
||||
|
||||
@ -581,7 +607,7 @@ export async function handlePlanAction(
|
||||
`请根据以下建议修改计划:${modification}`,
|
||||
extensionPath,
|
||||
"plan",
|
||||
serviceTier
|
||||
serviceTier,
|
||||
);
|
||||
}
|
||||
break;
|
||||
@ -636,7 +662,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();
|
||||
@ -653,7 +679,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();
|
||||
@ -669,7 +695,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();
|
||||
@ -718,7 +744,7 @@ async function handleFileOperation(
|
||||
newPath?: string;
|
||||
searchText?: string;
|
||||
replaceText?: string;
|
||||
}
|
||||
},
|
||||
) {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
|
||||
@ -734,7 +760,7 @@ async function handleFileOperation(
|
||||
text: responseText,
|
||||
});
|
||||
vscode.window.showInformationMessage(
|
||||
`文件创建成功: ${operation.filePath}`
|
||||
`文件创建成功: ${operation.filePath}`,
|
||||
);
|
||||
await historyManager.addAiMessage(responseText);
|
||||
break;
|
||||
@ -747,7 +773,7 @@ async function handleFileOperation(
|
||||
text: responseText,
|
||||
});
|
||||
vscode.window.showInformationMessage(
|
||||
`文件删除成功: ${operation.filePath}`
|
||||
`文件删除成功: ${operation.filePath}`,
|
||||
);
|
||||
await historyManager.addAiMessage(responseText);
|
||||
break;
|
||||
@ -783,7 +809,7 @@ async function handleFileOperation(
|
||||
text: responseText,
|
||||
});
|
||||
vscode.window.showInformationMessage(
|
||||
`文件重命名成功: ${operation.filePath} → ${operation.newPath}`
|
||||
`文件重命名成功: ${operation.filePath} → ${operation.newPath}`,
|
||||
);
|
||||
await historyManager.addAiMessage(responseText);
|
||||
break;
|
||||
@ -792,21 +818,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;
|
||||
@ -866,7 +900,7 @@ function getDefaultContent(filePath: string): string {
|
||||
*/
|
||||
export async function handleReadFile(
|
||||
panel: vscode.WebviewPanel,
|
||||
filePath: string
|
||||
filePath: string,
|
||||
) {
|
||||
try {
|
||||
const content = await readFileContent(filePath);
|
||||
@ -890,7 +924,7 @@ export async function handleCreateFile(
|
||||
panel: vscode.WebviewPanel,
|
||||
filePath: string,
|
||||
content: string,
|
||||
overwrite: boolean = false //是否覆盖
|
||||
overwrite: boolean = false, //是否覆盖
|
||||
) {
|
||||
try {
|
||||
if (overwrite) {
|
||||
@ -909,11 +943,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({
|
||||
@ -921,7 +958,7 @@ export async function handleCreateFile(
|
||||
error: error instanceof Error ? error.message : "创建文件失败",
|
||||
});
|
||||
vscode.window.showErrorMessage(
|
||||
`创建文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
`创建文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -932,7 +969,7 @@ export async function handleCreateFile(
|
||||
export async function handleUpdateFile(
|
||||
panel: vscode.WebviewPanel,
|
||||
filePath: string,
|
||||
content: string
|
||||
content: string,
|
||||
) {
|
||||
try {
|
||||
const oldContent = await readFileContent(filePath);
|
||||
@ -948,8 +985,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({
|
||||
@ -957,7 +994,7 @@ export async function handleUpdateFile(
|
||||
error: error instanceof Error ? error.message : "更新文件失败",
|
||||
});
|
||||
vscode.window.showErrorMessage(
|
||||
`更新文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
`更新文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -968,7 +1005,7 @@ export async function handleUpdateFile(
|
||||
export async function handleRenameFile(
|
||||
panel: vscode.WebviewPanel,
|
||||
oldPath: string,
|
||||
newPath: string
|
||||
newPath: string,
|
||||
) {
|
||||
try {
|
||||
await renameFile(oldPath, newPath);
|
||||
@ -979,7 +1016,7 @@ export async function handleRenameFile(
|
||||
message: "文件重命名成功",
|
||||
});
|
||||
vscode.window.showInformationMessage(
|
||||
`文件重命名成功: ${oldPath} → ${newPath}`
|
||||
`文件重命名成功: ${oldPath} → ${newPath}`,
|
||||
);
|
||||
} catch (error) {
|
||||
panel.webview.postMessage({
|
||||
@ -987,7 +1024,7 @@ export async function handleRenameFile(
|
||||
error: error instanceof Error ? error.message : "重命名文件失败",
|
||||
});
|
||||
vscode.window.showErrorMessage(
|
||||
`重命名文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
`重命名文件失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -999,7 +1036,7 @@ export async function handleReplaceInFile(
|
||||
panel: vscode.WebviewPanel,
|
||||
filePath: string,
|
||||
searchText: string,
|
||||
replaceText: string
|
||||
replaceText: string,
|
||||
) {
|
||||
try {
|
||||
const oldContent = await readFileContent(filePath);
|
||||
@ -1018,7 +1055,7 @@ export async function handleReplaceInFile(
|
||||
error: error instanceof Error ? error.message : "替换文件内容失败",
|
||||
});
|
||||
vscode.window.showErrorMessage(
|
||||
`替换文件内容失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
`替换文件内容失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1063,7 +1100,7 @@ function isVCDGenerationCommand(text: string): boolean {
|
||||
*/
|
||||
async function handleVCDGeneration(
|
||||
panel: vscode.WebviewPanel,
|
||||
extensionPath: string
|
||||
extensionPath: string,
|
||||
) {
|
||||
try {
|
||||
// 获取当前工作区路径
|
||||
@ -1090,7 +1127,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;
|
||||
@ -1165,12 +1202,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({
|
||||
@ -1199,12 +1239,12 @@ async function handleVCDGeneration(
|
||||
// 发送系统通知
|
||||
const notificationService = NotificationService.getInstance();
|
||||
notificationService.error(
|
||||
'IC Coder - 仿真失败',
|
||||
'VCD 文件生成失败,请查看错误信息',
|
||||
"IC Coder - 仿真失败",
|
||||
"VCD 文件生成失败,请查看错误信息",
|
||||
() => {
|
||||
// 点击通知时聚焦到面板
|
||||
panel.reveal();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
@ -1222,11 +1262,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();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1236,7 +1276,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);
|
||||
@ -1268,7 +1308,7 @@ export async function handleOptimizePrompt(
|
||||
*/
|
||||
export async function handleAcceptChange(
|
||||
panel: vscode.WebviewPanel,
|
||||
changeId: string
|
||||
changeId: string,
|
||||
) {
|
||||
try {
|
||||
const success = await changeTracker.acceptChange(changeId);
|
||||
@ -1276,14 +1316,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) {
|
||||
@ -1292,7 +1332,7 @@ export async function handleAcceptChange(
|
||||
command: "changeAccepted",
|
||||
changeId: changeId,
|
||||
success: false,
|
||||
error: String(error)
|
||||
error: String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1302,7 +1342,7 @@ export async function handleAcceptChange(
|
||||
*/
|
||||
export async function handleRejectChange(
|
||||
panel: vscode.WebviewPanel,
|
||||
changeId: string
|
||||
changeId: string,
|
||||
) {
|
||||
try {
|
||||
const success = await changeTracker.rejectChange(changeId);
|
||||
@ -1310,14 +1350,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) {
|
||||
@ -1326,7 +1366,7 @@ export async function handleRejectChange(
|
||||
command: "changeRejected",
|
||||
changeId: changeId,
|
||||
success: false,
|
||||
error: String(error)
|
||||
error: String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1337,18 +1377,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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1365,62 +1405,102 @@ 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Vivado 工具调用
|
||||
*/
|
||||
export async function handleVivadoToolCall(
|
||||
toolName: string,
|
||||
params: any
|
||||
): Promise<any> {
|
||||
try {
|
||||
switch (toolName) {
|
||||
case 'createVivadoProject':
|
||||
return await createVivadoProject(params);
|
||||
|
||||
case 'runVivadoSynthesis':
|
||||
return await runVivadoSynthesis(params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
command: toolName,
|
||||
executionTime: 0,
|
||||
output: '',
|
||||
error: `未知的工具: ${toolName}`
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
command: toolName,
|
||||
executionTime: 0,
|
||||
output: '',
|
||||
error: error.message || String(error)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
104
src/utils/tclGenerator.ts
Normal file
104
src/utils/tclGenerator.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* TCL 脚本生成器
|
||||
* 功能:生成 Vivado TCL 脚本
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 生成创建工程的 TCL 脚本
|
||||
*/
|
||||
export function generateCreateProjectTcl(
|
||||
projectName: string,
|
||||
projectDir: string,
|
||||
part: string,
|
||||
topModule: string,
|
||||
files: string[],
|
||||
constraints?: string,
|
||||
runSynthesis?: boolean
|
||||
): string {
|
||||
// 转换路径为 TCL 格式(正斜杠)
|
||||
const tclPath = (p: string) => p.replace(/\\/g, '/');
|
||||
|
||||
let tcl = `# 创建 Vivado 工程\n\n`;
|
||||
|
||||
tcl += `create_project ${projectName} {${tclPath(projectDir)}} -part ${part} -force\n\n`;
|
||||
|
||||
// 添加源文件
|
||||
tcl += `# 添加源文件\n`;
|
||||
files.forEach(file => {
|
||||
tcl += `add_files -norecurse {${tclPath(file)}}\n`;
|
||||
});
|
||||
tcl += `\n`;
|
||||
|
||||
// 添加约束文件
|
||||
if (constraints) {
|
||||
tcl += `# 添加约束文件\n`;
|
||||
tcl += `add_files -fileset constrs_1 -norecurse {${tclPath(constraints)}}\n\n`;
|
||||
}
|
||||
|
||||
// 设置顶层模块
|
||||
tcl += `# 设置顶层模块\n`;
|
||||
tcl += `set_property top ${topModule} [current_fileset]\n\n`;
|
||||
|
||||
if (runSynthesis) {
|
||||
tcl += `# 执行综合\n`;
|
||||
tcl += `launch_runs synth_1\n`;
|
||||
tcl += `wait_on_run synth_1\n\n`;
|
||||
tcl += `# 打开综合结果\n`;
|
||||
tcl += `open_run synth_1\n\n`;
|
||||
tcl += `# 生成报告\n`;
|
||||
tcl += `report_utilization -file {${tclPath(path.join(projectDir, `${projectName}_utilization.rpt`))}}\n`;
|
||||
tcl += `report_timing_summary -file {${tclPath(path.join(projectDir, `${projectName}_timing.rpt`))}}\n\n`;
|
||||
}
|
||||
|
||||
return tcl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成综合的 TCL 脚本
|
||||
*/
|
||||
export function generateSynthesisTcl(
|
||||
projectPath: string | undefined,
|
||||
part: string,
|
||||
topModule: string,
|
||||
files?: string[],
|
||||
constraints?: string,
|
||||
outputDir?: string
|
||||
): string {
|
||||
const tclPath = (p: string) => p.replace(/\\/g, '/');
|
||||
let tcl = `# Vivado 综合\n\n`;
|
||||
|
||||
if (projectPath) {
|
||||
// 使用现有工程
|
||||
tcl += `open_project {${tclPath(projectPath)}}\n\n`;
|
||||
} else {
|
||||
// 无工程模式
|
||||
if (!files || files.length === 0) {
|
||||
throw new Error('无工程模式需要提供源文件');
|
||||
}
|
||||
tcl += `# 读取源文件\n`;
|
||||
files.forEach(file => {
|
||||
tcl += `read_verilog {${tclPath(file)}}\n`;
|
||||
});
|
||||
tcl += `\n`;
|
||||
|
||||
if (constraints) {
|
||||
tcl += `read_xdc {${tclPath(constraints)}}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
tcl += `# 执行综合\n`;
|
||||
tcl += `synth_design -top ${topModule} -part ${part}\n\n`;
|
||||
|
||||
if (outputDir) {
|
||||
const dcpFile = tclPath(path.join(outputDir, `${topModule}_synth.dcp`));
|
||||
tcl += `# 保存检查点\n`;
|
||||
tcl += `write_checkpoint -force {${dcpFile}}\n\n`;
|
||||
tcl += `# 生成报告\n`;
|
||||
tcl += `report_utilization -file {${tclPath(path.join(outputDir, `${topModule}_utilization.rpt`))}}\n`;
|
||||
tcl += `report_timing_summary -file {${tclPath(path.join(outputDir, `${topModule}_timing.rpt`))}}\n`;
|
||||
}
|
||||
|
||||
return tcl;
|
||||
}
|
||||
87
src/utils/vivadoConfig.ts
Normal file
87
src/utils/vivadoConfig.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Vivado 配置管理
|
||||
* 功能:读取和验证 Vivado 配置
|
||||
* 依赖:vscode
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface VivadoConfig {
|
||||
enabled: boolean;
|
||||
executablePath: string;
|
||||
workingDir: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Vivado 配置
|
||||
*/
|
||||
export function getVivadoConfig(): VivadoConfig | null {
|
||||
// 优先读取项目配置
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (workspaceFolder) {
|
||||
const projectConfigPath = path.join(
|
||||
workspaceFolder.uri.fsPath,
|
||||
'.vscode',
|
||||
'ic-coder-vivado.json'
|
||||
);
|
||||
if (fs.existsSync(projectConfigPath)) {
|
||||
const content = fs.readFileSync(projectConfigPath, 'utf-8');
|
||||
return JSON.parse(content).vivado;
|
||||
}
|
||||
}
|
||||
|
||||
// 读取全局配置
|
||||
const config = vscode.workspace.getConfiguration('ic-coder');
|
||||
const vivadoConfig = config.get<VivadoConfig>('vivado');
|
||||
|
||||
if (vivadoConfig) {
|
||||
return vivadoConfig;
|
||||
}
|
||||
|
||||
// 自动检测 Vivado
|
||||
return autoDetectVivado();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置
|
||||
*/
|
||||
export function validateConfig(config: VivadoConfig): string | null {
|
||||
if (!config.enabled) {
|
||||
return 'Vivado 未启用';
|
||||
}
|
||||
// 如果是完整路径,检查文件是否存在
|
||||
if (path.isAbsolute(config.executablePath) && !fs.existsSync(config.executablePath)) {
|
||||
return `Vivado 可执行文件不存在: ${config.executablePath}`;
|
||||
}
|
||||
// 环境变量命令不检查,运行时会自动报错
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析工作目录
|
||||
*/
|
||||
export function resolveWorkingDir(workingDir: string): string {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
if (workspaceFolder) {
|
||||
return workingDir.replace('${workspaceFolder}', workspaceFolder.uri.fsPath);
|
||||
}
|
||||
return workingDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动检测 Vivado
|
||||
*/
|
||||
function autoDetectVivado(): VivadoConfig | null {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
|
||||
// 默认使用环境变量中的 vivado 命令
|
||||
return {
|
||||
enabled: true,
|
||||
executablePath: 'vivado',
|
||||
workingDir: workspaceFolder
|
||||
? path.join(workspaceFolder.uri.fsPath, 'vivado_projects')
|
||||
: path.join(process.env.USERPROFILE || 'C:\\Users\\Default', 'vivado_projects')
|
||||
};
|
||||
}
|
||||
246
src/utils/vivadoRunner.ts
Normal file
246
src/utils/vivadoRunner.ts
Normal file
@ -0,0 +1,246 @@
|
||||
/**
|
||||
* Vivado 执行器
|
||||
* 功能:执行 Vivado 命令
|
||||
* 依赖:vivadoConfig, tclGenerator
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { spawn } from 'child_process';
|
||||
import { getVivadoConfig, validateConfig, resolveWorkingDir } from './vivadoConfig';
|
||||
import { generateCreateProjectTcl, generateSynthesisTcl } from './tclGenerator';
|
||||
|
||||
export interface VivadoToolResponse {
|
||||
success: boolean;
|
||||
command: string;
|
||||
executionTime: number;
|
||||
output: string;
|
||||
error?: string;
|
||||
outputFiles?: string[];
|
||||
reports?: {
|
||||
resources?: string;
|
||||
timing?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Vivado 工程
|
||||
*/
|
||||
export async function createVivadoProject(params: {
|
||||
projectName: string;
|
||||
part: string;
|
||||
topModule: string;
|
||||
files: string[];
|
||||
constraints?: string;
|
||||
mode: 'gui' | 'batch';
|
||||
runSynthesis?: boolean;
|
||||
}): Promise<VivadoToolResponse> {
|
||||
const startTime = Date.now();
|
||||
|
||||
// 读取配置
|
||||
const config = getVivadoConfig();
|
||||
if (!config) {
|
||||
return {
|
||||
success: false,
|
||||
command: 'create_project',
|
||||
executionTime: 0,
|
||||
output: '',
|
||||
error: 'Vivado 未配置'
|
||||
};
|
||||
}
|
||||
|
||||
// 验证配置
|
||||
const configError = validateConfig(config);
|
||||
if (configError) {
|
||||
return {
|
||||
success: false,
|
||||
command: 'create_project',
|
||||
executionTime: 0,
|
||||
output: '',
|
||||
error: configError
|
||||
};
|
||||
}
|
||||
|
||||
// 准备工作目录
|
||||
const workingDir = resolveWorkingDir(config.workingDir);
|
||||
if (!fs.existsSync(workingDir)) {
|
||||
fs.mkdirSync(workingDir, { recursive: true });
|
||||
}
|
||||
|
||||
const projectDir = path.join(workingDir, params.projectName);
|
||||
|
||||
// 生成 TCL 脚本
|
||||
const tclScript = generateCreateProjectTcl(
|
||||
params.projectName,
|
||||
projectDir,
|
||||
params.part,
|
||||
params.topModule,
|
||||
params.files,
|
||||
params.constraints,
|
||||
params.runSynthesis
|
||||
);
|
||||
|
||||
const tclPath = path.join(workingDir, 'create_project.tcl');
|
||||
fs.writeFileSync(tclPath, tclScript);
|
||||
|
||||
// 执行 Vivado
|
||||
const result = await executeVivado(
|
||||
config.executablePath,
|
||||
tclPath,
|
||||
workingDir,
|
||||
params.mode
|
||||
);
|
||||
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
// 查找产出文件
|
||||
const xprFile = path.join(projectDir, `${params.projectName}.xpr`);
|
||||
const outputFiles = fs.existsSync(xprFile) ? [xprFile] : [];
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
command: 'create_project',
|
||||
executionTime,
|
||||
output: result.output,
|
||||
error: result.error,
|
||||
outputFiles
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 Vivado 综合
|
||||
*/
|
||||
export async function runVivadoSynthesis(params: {
|
||||
projectPath?: string;
|
||||
part: string;
|
||||
topModule: string;
|
||||
files?: string[];
|
||||
constraints?: string;
|
||||
mode: 'gui' | 'batch';
|
||||
}): Promise<VivadoToolResponse> {
|
||||
const startTime = Date.now();
|
||||
|
||||
const config = getVivadoConfig();
|
||||
if (!config) {
|
||||
return {
|
||||
success: false,
|
||||
command: 'synthesis',
|
||||
executionTime: 0,
|
||||
output: '',
|
||||
error: 'Vivado 未配置'
|
||||
};
|
||||
}
|
||||
|
||||
const configError = validateConfig(config);
|
||||
if (configError) {
|
||||
return {
|
||||
success: false,
|
||||
command: 'synthesis',
|
||||
executionTime: 0,
|
||||
output: '',
|
||||
error: configError
|
||||
};
|
||||
}
|
||||
|
||||
const workingDir = resolveWorkingDir(config.workingDir);
|
||||
if (!fs.existsSync(workingDir)) {
|
||||
fs.mkdirSync(workingDir, { recursive: true });
|
||||
}
|
||||
|
||||
const outputDir = path.join(workingDir, 'synth_output');
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
const tclScript = generateSynthesisTcl(
|
||||
params.projectPath,
|
||||
params.part,
|
||||
params.topModule,
|
||||
params.files,
|
||||
params.constraints,
|
||||
outputDir
|
||||
);
|
||||
|
||||
const tclPath = path.join(workingDir, 'synthesis.tcl');
|
||||
fs.writeFileSync(tclPath, tclScript);
|
||||
|
||||
const result = await executeVivado(
|
||||
config.executablePath,
|
||||
tclPath,
|
||||
workingDir,
|
||||
params.mode
|
||||
);
|
||||
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
const dcpFile = path.join(outputDir, `${params.topModule}_synth.dcp`);
|
||||
const utilizationRpt = path.join(outputDir, `${params.topModule}_utilization.rpt`);
|
||||
const timingRpt = path.join(outputDir, `${params.topModule}_timing.rpt`);
|
||||
|
||||
const outputFiles = [];
|
||||
if (fs.existsSync(dcpFile)) outputFiles.push(dcpFile);
|
||||
if (fs.existsSync(utilizationRpt)) outputFiles.push(utilizationRpt);
|
||||
if (fs.existsSync(timingRpt)) outputFiles.push(timingRpt);
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
command: 'synthesis',
|
||||
executionTime,
|
||||
output: result.output,
|
||||
error: result.error,
|
||||
outputFiles
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 Vivado 命令
|
||||
*/
|
||||
async function executeVivado(
|
||||
executablePath: string,
|
||||
tclPath: string,
|
||||
workingDir: string,
|
||||
mode: 'gui' | 'batch'
|
||||
): Promise<{ success: boolean; output: string; error?: string }> {
|
||||
return new Promise((resolve) => {
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
const args = mode === 'gui'
|
||||
? ['-source', tclPath]
|
||||
: ['-mode', 'batch', '-source', tclPath];
|
||||
|
||||
const process = spawn(executablePath, args, {
|
||||
cwd: workingDir,
|
||||
shell: true
|
||||
});
|
||||
|
||||
process.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
process.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
|
||||
process.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ success: true, output });
|
||||
} else {
|
||||
resolve({
|
||||
success: false,
|
||||
output,
|
||||
error: errorOutput || `执行失败,退出码: ${code}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
process.on('error', (err) => {
|
||||
resolve({
|
||||
success: false,
|
||||
output,
|
||||
error: `启动 Vivado 失败: ${err.message}`
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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">
|
||||
@ -62,12 +69,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 {
|
||||
@ -220,15 +259,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) {
|
||||
// 先检查邀请码验证状态
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
220
src/views/messageRenderer.ts
Normal file
220
src/views/messageRenderer.ts
Normal file
@ -0,0 +1,220 @@
|
||||
/**
|
||||
* 消息渲染脚本模块
|
||||
* 功能:消息渲染、滚动控制、工具状态显示
|
||||
* 依赖:toolHelpers, textFormatter, waveformPreviewContent, agentCard, planCard, codeHighlight
|
||||
* 使用场景:webview 中的消息显示逻辑
|
||||
*/
|
||||
|
||||
import { collapseIconSvg } from "../constants/toolIcons";
|
||||
import { getWaveformPreviewScript } from "./waveformPreviewContent";
|
||||
import { getAgentCardScript } from "./agentCard";
|
||||
import { getPlanCardScript } from "./planCard";
|
||||
import { getCodeHighlightScript } from "../components/codeHighlight";
|
||||
|
||||
export function getMessageRendererScript(): string {
|
||||
return `
|
||||
${getAgentCardScript()}
|
||||
${getPlanCardScript()}
|
||||
|
||||
const toolCollapseStates = new Map();
|
||||
let shouldAutoScroll = true;
|
||||
let lastScrollHeight = 0;
|
||||
|
||||
function isUserNearBottom() {
|
||||
const threshold = 50;
|
||||
return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold;
|
||||
}
|
||||
|
||||
messagesEl.addEventListener('scroll', () => {
|
||||
const isAtBottom = isUserNearBottom();
|
||||
if (isAtBottom) {
|
||||
shouldAutoScroll = true;
|
||||
} else {
|
||||
if (messagesEl.scrollHeight === lastScrollHeight) {
|
||||
shouldAutoScroll = false;
|
||||
}
|
||||
}
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
});
|
||||
|
||||
function smartScrollToBottom() {
|
||||
if (shouldAutoScroll) {
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function addMessage(text, sender) {
|
||||
const div = document.createElement('div');
|
||||
div.className = \`message \${sender}-message\`;
|
||||
if (sender === 'bot') {
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
const messageContent = document.createElement('span');
|
||||
messageContent.textContent = text;
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
copyBtn.onclick = () => copyMessage(text, copyBtn);
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'action-btn';
|
||||
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||
const dislikeBtn = document.createElement('button');
|
||||
dislikeBtn.className = 'action-btn';
|
||||
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||
actionsDiv.appendChild(messageContent);
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
actionsDiv.appendChild(likeBtn);
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
div.appendChild(actionsDiv);
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
hideHeaderIfNeeded();
|
||||
}
|
||||
messagesEl.appendChild(div);
|
||||
smartScrollToBottom();
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
|
||||
function hideHeaderIfNeeded() {
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
|
||||
function copyMessage(text, button) {
|
||||
// 从按钮的父消息元素中获取实际文本内容
|
||||
const messageDiv = button.closest('.message');
|
||||
const messageContent = messageDiv?.querySelector('.message-content, div:not(.message-actions)');
|
||||
const textToCopy = messageContent ? messageContent.textContent : text;
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474c-6.1-7.7-15.3-12.2-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1 0.4-12.8-6.3-12.8z" fill="currentColor"/></svg>\`;
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleLike(button) {
|
||||
const isActive = button.classList.contains('active');
|
||||
const parent = button.parentElement;
|
||||
parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active'));
|
||||
if (!isActive) {
|
||||
button.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDislike(button) {
|
||||
const isActive = button.classList.contains('active');
|
||||
const parent = button.parentElement;
|
||||
parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active'));
|
||||
if (!isActive) {
|
||||
button.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function updateOrCreateStreamingMessage(text) {
|
||||
hideLoadingIndicator();
|
||||
if (!currentStreamingMessage) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message bot-message streaming';
|
||||
const messageContent = document.createElement('div');
|
||||
messageContent.className = 'message-content';
|
||||
messageContent.textContent = text;
|
||||
div.appendChild(messageContent);
|
||||
messagesEl.appendChild(div);
|
||||
currentStreamingMessage = div;
|
||||
} else {
|
||||
const messageContent = currentStreamingMessage.querySelector('.message-content');
|
||||
if (messageContent) {
|
||||
messageContent.textContent = text;
|
||||
}
|
||||
}
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function finalizeStreamingMessage(finalText) {
|
||||
if (currentStreamingMessage) {
|
||||
const messageContent = currentStreamingMessage.querySelector('.message-content');
|
||||
if (messageContent) {
|
||||
messageContent.textContent = finalText;
|
||||
}
|
||||
currentStreamingMessage.classList.remove('streaming');
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
copyBtn.onclick = () => copyMessage(finalText, copyBtn);
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
currentStreamingMessage.appendChild(actionsDiv);
|
||||
currentStreamingMessage = null;
|
||||
}
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function showLoadingIndicator(text) {
|
||||
hideLoadingIndicator();
|
||||
loadingIndicator = document.createElement('div');
|
||||
loadingIndicator.className = 'message bot-message loading-message';
|
||||
loadingIndicator.innerHTML = \`
|
||||
<div class="loading-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<span class="loading-text">\${text}</span>
|
||||
\`;
|
||||
messagesEl.appendChild(loadingIndicator);
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function hideLoadingIndicator() {
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.remove();
|
||||
loadingIndicator = null;
|
||||
}
|
||||
}
|
||||
|
||||
function addToolStatus(toolName, status, detail) {
|
||||
const statusIcons = {
|
||||
start: '🔧',
|
||||
complete: '✅',
|
||||
error: '❌'
|
||||
};
|
||||
const statusTexts = {
|
||||
start: '正在执行',
|
||||
complete: '执行完成',
|
||||
error: '执行失败'
|
||||
};
|
||||
const div = document.createElement('div');
|
||||
div.className = \`message tool-status tool-\${status}\`;
|
||||
div.innerHTML = \`
|
||||
<span class="tool-icon">\${statusIcons[status]}</span>
|
||||
<span class="tool-name">\${getToolDisplayName(toolName)}</span>
|
||||
<span class="tool-status-text">\${statusTexts[status]}</span>
|
||||
\${detail ? \`<div class="tool-detail">\${detail}</div>\` : ''}
|
||||
\`;
|
||||
messagesEl.appendChild(div);
|
||||
smartScrollToBottom();
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
|
||||
${getWaveformPreviewScript()}
|
||||
${getCodeHighlightScript()}
|
||||
`;
|
||||
}
|
||||
632
src/views/messageStyles.ts
Normal file
632
src/views/messageStyles.ts
Normal file
@ -0,0 +1,632 @@
|
||||
/**
|
||||
* 消息样式模块
|
||||
* 功能:提供消息区域的所有 CSS 样式
|
||||
* 依赖:agentCard, planCard, codeHighlight, waveformPreviewContent
|
||||
* 使用场景:webview 样式注入
|
||||
*/
|
||||
|
||||
import { getAgentCardStyles } from "./agentCard";
|
||||
import { getPlanCardStyles } from "./planCard";
|
||||
import { getCodeHighlightStyles } from "../components/codeHighlight";
|
||||
import { getWaveformPreviewContent } from "./waveformPreviewContent";
|
||||
|
||||
export function getMessageAreaStyles(): string {
|
||||
return `
|
||||
.messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 15px;
|
||||
min-height: 0;
|
||||
}
|
||||
.message {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.user-message {
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
margin-left: auto;
|
||||
width: fit-content;
|
||||
max-width: 80%;
|
||||
}
|
||||
.bot-message {
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
color: var(--vscode-foreground);
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.message-actions {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
margin-left: 12px;
|
||||
opacity: 0.85;
|
||||
transition: opacity 0.2s ease;
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
}
|
||||
.message-actions > span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.message-actions:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.action-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vscode-foreground);
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s ease;
|
||||
position: relative;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.action-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.action-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.action-btn.active {
|
||||
color: var(--vscode-button-background);
|
||||
opacity: 1;
|
||||
}
|
||||
.action-btn .action-tooltip {
|
||||
visibility: hidden;
|
||||
width: auto;
|
||||
background: #1e1e1e;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 4px 8px;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
bottom: 125%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(5px);
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;white-space: nowrap;pointer-events: none;
|
||||
}
|
||||
.action-btn .action-tooltip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: #1e1e1e transparent transparent transparent;
|
||||
}
|
||||
.action-btn .action-tooltip::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: rgba(255, 255, 255, 0.2) transparent transparent transparent;
|
||||
z-index: -1;
|
||||
}
|
||||
.action-btn:hover .action-tooltip {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.streaming .message-content {
|
||||
border-right: 2px solid var(--vscode-focusBorder);
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
@keyframes blink {
|
||||
0%, 50% { border-color: var(--vscode-focusBorder); }
|
||||
51%, 100% { border-color: transparent; }
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 4px;}
|
||||
.loading-dots span {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--vscode-focusBorder);
|
||||
animation: loadingDot 1.4s infinite ease-in-out;
|
||||
}
|
||||
.loading-dots span:nth-child(1) { animation-delay: 0s; }
|
||||
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@keyframes loadingDot {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
.loading-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tool-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
}
|
||||
.tool-status.tool-start {
|
||||
border-left: 3px solid var(--vscode-charts-blue);
|
||||
}
|
||||
.tool-status.tool-complete {
|
||||
border-left: 3px solid var(--vscode-charts-green);
|
||||
}
|
||||
.tool-status.tool-error {
|
||||
border-left: 3px solid var(--vscode-charts-red);
|
||||
}
|
||||
.tool-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
.tool-name {
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.tool-status-text {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.tool-detail {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
white-space: pre-wrap;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.question-message {
|
||||
padding: 16px;
|
||||
}
|
||||
.question-text {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.question-option {
|
||||
padding: 8px 16px;
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border: 1px solid #007ACC;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.question-option:hover {
|
||||
background: #005a9e;
|
||||
border-color: #005a9e;
|
||||
}
|
||||
.question-option.selected {
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border-color: #007ACC;
|
||||
}
|
||||
.question-message.answered .question-option:not(.selected) {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.custom-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.custom-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.custom-submit {
|
||||
padding: 8px 16px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.custom-submit:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.question-message.answered .custom-input-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.segmented-message {
|
||||
padding: 0;
|
||||
}
|
||||
.message-segment {
|
||||
padding: 10px 0;
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.segment-text h1,
|
||||
.segment-text h2,
|
||||
.segment-text h3,
|
||||
.question-text h1,
|
||||
.question-text h2,
|
||||
.question-text h3 {
|
||||
margin: 0px 0 -10px 0;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.segment-text h1,
|
||||
.question-text h1 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.segment-text h2,
|
||||
.question-text h2 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
.segment-text h3,
|
||||
.question-text h3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.segment-text ul,
|
||||
.segment-text ol,
|
||||
.question-text ul,
|
||||
.question-text ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.segment-text li,
|
||||
.question-text li {
|
||||
line-height: 1;
|
||||
}
|
||||
.segment-text strong,
|
||||
.question-text strong {
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.segment-text em,
|
||||
.question-text em {
|
||||
font-style: italic;
|
||||
}
|
||||
.segment-text a,
|
||||
.question-text a {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
text-decoration: none;
|
||||
}
|
||||
.segment-text a:hover,
|
||||
.question-text a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.segment-text p,
|
||||
.question-text p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.segment-text code,
|
||||
.question-text code {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.segment-tool {
|
||||
margin: 4px 0;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.segment-tool.low-profile {
|
||||
margin: 25px 0px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
.tool-segment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool-segment-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
.tool-segment-name {
|
||||
font-weight: normal;
|
||||
}
|
||||
.tool-segment-result {
|
||||
display: inline;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-left: 6px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 500px;
|
||||
}
|
||||
.tool-collapse-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool-collapse-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.icon-expanded svg path {
|
||||
fill: #007ACC !important;
|
||||
}
|
||||
.tool-segment-header.collapsed .tool-collapse-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.tool-segment-header:not(.collapsed) .tool-collapse-icon {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
.tool-file-write-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-write-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-file-read-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-read-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-file-delete-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-delete-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-syntax-check-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-syntax-check-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-search-code-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-search-code-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-save-knowledge-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-save-knowledge-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-simulation-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-simulation-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-waveform-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-waveform-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-knowledge-load-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-knowledge-load-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-state-transition-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-state-transition-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-segment-content {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
.tool-segment-content.collapsed {
|
||||
max-height: 0;
|
||||
}
|
||||
.tool-segment-description {
|
||||
margin: 25px 0 0 0px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vscode-foreground);
|
||||
line-height: 1.4;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-header {
|
||||
opacity: 0.65;
|
||||
font-size: 12px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-icon {
|
||||
opacity: 0.55;
|
||||
font-size: 11px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-name {
|
||||
font-weight: 300;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-result {
|
||||
opacity: 0.7;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-question {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-radius: 6px;
|
||||
margin: 8px 0;
|
||||
padding: 12px 35px;
|
||||
border-left: 3px solid var(--vscode-charts-orange);
|
||||
}
|
||||
.segment-question .question-text {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.segment-question .question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.segment-question .question-option {
|
||||
padding: 8px 16px;
|
||||
background: #3d3f41;
|
||||
color: var(--vscode-foreground);
|
||||
border: 1px solid #474747;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-question .question-option:hover {
|
||||
background: #005a9e;
|
||||
border-color: #005a9e;
|
||||
}
|
||||
.segment-question .question-option.selected {
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border-color: #007ACC;
|
||||
}
|
||||
.segment-question.answered .question-option:not(.selected) {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.segment-question .custom-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.segment-question .custom-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
.segment-question .custom-submit {
|
||||
padding: 8px 16px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.segment-question .custom-submit:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.segment-question.answered .custom-input-container {
|
||||
display: none;
|
||||
}
|
||||
.question-segment .question-text {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.question-segment .question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.question-opt {
|
||||
padding: 4px 10px;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;}
|
||||
|
||||
${getAgentCardStyles()}
|
||||
|
||||
${getPlanCardStyles()}
|
||||
|
||||
${getCodeHighlightStyles()}
|
||||
|
||||
${getWaveformPreviewContent()}
|
||||
`;
|
||||
}
|
||||
@ -59,7 +59,7 @@ export function getPlanCardStyles(): string {
|
||||
.plan-summary h2 { font-size: 16px; }
|
||||
.plan-summary h3 { font-size: 14px; }
|
||||
.plan-summary h4 { font-size: 13px; }
|
||||
.plan-summary p { margin: 8px 0; }
|
||||
.plan-summary p { margin: 8px 0; letter-spacing: 0.5px; }
|
||||
.plan-summary ul, .plan-summary ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 0;
|
||||
|
||||
@ -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>
|
||||
|
||||
118
src/views/questionHandler.ts
Normal file
118
src/views/questionHandler.ts
Normal file
@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 问题处理脚本模块
|
||||
* 功能:用户问题交互逻辑
|
||||
* 依赖:textFormatter
|
||||
* 使用场景:webview 中的问题回答处理
|
||||
*/
|
||||
|
||||
export function getQuestionHandlerScript(): string {
|
||||
return `
|
||||
const answeredQuestions = new Map();
|
||||
|
||||
function handleQuestionAnswer(askId, answer, questionDiv) {
|
||||
console.log('[WebView] 用户选择答案:', askId, answer);
|
||||
questionDiv.classList.add('answered');
|
||||
const options = questionDiv.querySelectorAll('.question-option');
|
||||
options.forEach(opt => {
|
||||
if (opt.textContent === answer) {
|
||||
opt.classList.add('selected');
|
||||
}
|
||||
});
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: askId,
|
||||
selected: [answer],
|
||||
customInput: answer
|
||||
});
|
||||
}
|
||||
|
||||
function handleQuestionAnswerInSegment(askId, answer, segmentDiv) {
|
||||
console.log('[WebView] 段落中用户选择答案:', askId, answer);
|
||||
answeredQuestions.set(askId, answer);
|
||||
segmentDiv.classList.add('answered');
|
||||
const options = segmentDiv.querySelectorAll('.question-option');
|
||||
options.forEach(opt => {
|
||||
if (opt.getAttribute('data-option') === answer) {
|
||||
opt.classList.add('selected');
|
||||
}
|
||||
});
|
||||
const customContainer = segmentDiv.querySelector('.custom-input-container');
|
||||
if (customContainer) {
|
||||
customContainer.style.display = 'none';
|
||||
}
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: askId,
|
||||
selected: [answer],
|
||||
customInput: answer
|
||||
});
|
||||
}
|
||||
|
||||
function handleMultiQuestionAnswer(askId, answers, segmentDiv) {
|
||||
console.log('[WebView] 多问题答案提交:', askId, answers);
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
function showQuestion(askId, question, options) {
|
||||
console.log('[WebView] showQuestion 被调用:', askId, question, options);
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message bot-message question-message';
|
||||
div.setAttribute('data-ask-id', askId);
|
||||
const questionText = document.createElement('div');
|
||||
questionText.className = 'question-text';
|
||||
questionText.textContent = question;
|
||||
div.appendChild(questionText);
|
||||
const optionsContainer = document.createElement('div');
|
||||
optionsContainer.className = 'question-options';
|
||||
options.forEach((option, index) => {
|
||||
const optionBtn = document.createElement('button');
|
||||
optionBtn.className = 'question-option';
|
||||
optionBtn.textContent = option;
|
||||
optionBtn.onclick = () => handleQuestionAnswer(askId, option, div);
|
||||
optionsContainer.appendChild(optionBtn);
|
||||
});
|
||||
div.appendChild(optionsContainer);
|
||||
const customContainer = document.createElement('div');
|
||||
customContainer.className = 'custom-input-container';
|
||||
const customInput = document.createElement('input');
|
||||
customInput.type = 'text';
|
||||
customInput.className = 'custom-input';
|
||||
customInput.placeholder = '输入其他答案...';
|
||||
const customSubmit = document.createElement('button');
|
||||
customSubmit.className = 'custom-submit';
|
||||
customSubmit.textContent = '提交';
|
||||
customSubmit.onclick = () => {
|
||||
const customValue = customInput.value.trim();
|
||||
if (customValue) {
|
||||
handleQuestionAnswer(askId, customValue, div);
|
||||
}
|
||||
};
|
||||
customContainer.appendChild(customInput);
|
||||
customContainer.appendChild(customSubmit);
|
||||
div.appendChild(customContainer);
|
||||
messagesEl.appendChild(div);
|
||||
smartScrollToBottom();
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
`;
|
||||
}
|
||||
272
src/views/segmentRenderer.ts
Normal file
272
src/views/segmentRenderer.ts
Normal file
@ -0,0 +1,272 @@
|
||||
/**
|
||||
* 分段消息渲染脚本模块
|
||||
* 功能:实时更新分段消息、工具调用展示
|
||||
* 依赖:toolHelpers, textFormatter, waveformPreviewContent
|
||||
* 使用场景:webview 中的分段消息渲染
|
||||
*/
|
||||
|
||||
export function getSegmentRendererScript(): string {
|
||||
return `
|
||||
function updateSegmentsRealtime(segments, isComplete) {
|
||||
if (!isComplete && (!segments || segments.length === 0)) return;
|
||||
|
||||
if (!currentSegmentedMessage) {
|
||||
if (currentStreamingMessage) {
|
||||
currentStreamingMessage.remove();
|
||||
currentStreamingMessage = null;
|
||||
}
|
||||
const toolStatuses = messagesEl.querySelectorAll('.tool-status');
|
||||
toolStatuses.forEach(el => el.remove());
|
||||
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;
|
||||
}
|
||||
|
||||
if (currentSegmentedMessage) {
|
||||
const toolHeaders = currentSegmentedMessage.querySelectorAll('.tool-segment-header[data-collapsible="true"]');
|
||||
toolHeaders.forEach((header, idx) => {
|
||||
const isCollapsed = header.classList.contains('collapsed');
|
||||
toolCollapseStates.set(idx, isCollapsed);
|
||||
});
|
||||
}
|
||||
|
||||
if (!isComplete) {
|
||||
currentSegmentedMessage.innerHTML = '';
|
||||
}
|
||||
|
||||
const mergedSegments = [];
|
||||
let i = 0;
|
||||
while (i < (segments?.length || 0)) {
|
||||
const segment = segments[i];
|
||||
if (segment.type === 'tool') {
|
||||
let count = 1;
|
||||
while (i + count < segments.length &&
|
||||
segments[i + count].type === 'tool' &&
|
||||
segments[i + count].toolName === segment.toolName) {
|
||||
count++;
|
||||
}
|
||||
mergedSegments.push({ ...segment, toolCount: count });
|
||||
i += count;
|
||||
} else {
|
||||
mergedSegments.push(segment);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
let toolIndex = 0;
|
||||
mergedSegments.forEach((segment, index) => {
|
||||
const segmentDiv = document.createElement('div');
|
||||
segmentDiv.className = 'message-segment segment-' + segment.type;
|
||||
|
||||
if (segment.type === 'text' && segment.content) {
|
||||
segmentDiv.className += ' segment-text';
|
||||
segmentDiv.innerHTML = formatText(segment.content);
|
||||
} else if (segment.type === 'tool') {
|
||||
if (segment.toolName === 'spawnExplorer') return;
|
||||
segmentDiv.className += ' low-profile';
|
||||
const toolResult = segment.toolResult || '';
|
||||
const toolCount = segment.toolCount || 1;
|
||||
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||||
const toolDescription = segment.toolDescription || '';
|
||||
const shouldCollapse = toolResult && toolResult.length > 60;
|
||||
const savedState = toolCollapseStates.get(toolIndex);
|
||||
const isCollapsed = savedState !== undefined ? savedState : shouldCollapse;
|
||||
const currentToolIndex = toolIndex;
|
||||
toolIndex++;
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="tool-segment-header\${isCollapsed ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}" data-tool-index="\${currentToolIndex}">
|
||||
\${shouldCollapse ? \`<span class="tool-collapse-icon">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
|
||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
|
||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||
</div>
|
||||
\${shouldCollapse ? \`<div class="tool-segment-content\${isCollapsed ? ' collapsed' : ''}" style="max-height:\${isCollapsed ? '0' : 'none'}"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||
\${toolDescription ? \`<p class="tool-segment-description">\${toolDescription}</p>\` : ''}
|
||||
\`;
|
||||
|
||||
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
||||
if (typeof createWaveformPreview === 'function') {
|
||||
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
|
||||
if (vcdPaths.length > 0) {
|
||||
vcdPaths.forEach(vcdInfo => {
|
||||
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
});
|
||||
} else {
|
||||
let vcdPath = segment.vcdFilePath;
|
||||
if (!vcdPath && segment.toolResult) {
|
||||
const match = String(segment.toolResult).match(/(?:路径\\s*[::]\\s*|已生成[::]\\s*)(.+\\.vcd)/);
|
||||
if (match && match[1]) {
|
||||
vcdPath = match[1].trim();
|
||||
}
|
||||
}
|
||||
if (vcdPath) {
|
||||
const fileName = segment.fileName || vcdPath.split(/[\\\\\\/]/).pop() || 'waveform.vcd';
|
||||
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('[VCD Preview] createWaveformPreview function not found');
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCollapse) {
|
||||
setTimeout(() => {
|
||||
const header = segmentDiv.querySelector('.tool-segment-header');
|
||||
const content = segmentDiv.querySelector('.tool-segment-content');
|
||||
if (header && content) {
|
||||
header.addEventListener('click', function() {
|
||||
const isCollapsed = header.classList.contains('collapsed');
|
||||
const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
|
||||
if (isCollapsed) {
|
||||
header.classList.remove('collapsed');
|
||||
content.classList.remove('collapsed');
|
||||
content.style.maxHeight = content.scrollHeight + 'px';
|
||||
toolCollapseStates.set(toolIdx, false);
|
||||
} else {
|
||||
header.classList.add('collapsed');
|
||||
content.classList.add('collapsed');
|
||||
content.style.maxHeight = '0';
|
||||
toolCollapseStates.set(toolIdx, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
} else if (segment.type === 'question') {
|
||||
segmentDiv.className += ' segment-question';
|
||||
const questions = segment.questions || (segment.question ? [{
|
||||
question: segment.question,
|
||||
options: segment.options || [],
|
||||
multiSelect: false
|
||||
}] : []);
|
||||
const isAnswered = answeredQuestions.has(segment.askId);
|
||||
const savedAnswers = answeredQuestions.get(segment.askId) || {};
|
||||
if (isAnswered) {
|
||||
segmentDiv.classList.add('answered');
|
||||
}
|
||||
|
||||
const questionsHtml = questions.map((q, qIndex) => {
|
||||
const inputType = q.multiSelect ? 'checkbox' : 'radio';
|
||||
const inputName = \`q\${qIndex}\`;
|
||||
const selectedAnswers = savedAnswers[qIndex] || [];
|
||||
let optionsHtml;
|
||||
if (!q.options || q.options.length === 0) {
|
||||
const savedText = selectedAnswers[0] || '';
|
||||
optionsHtml = \`<textarea class="question-text-input" name="\${inputName}" placeholder="请输入您的答案..." style="width:100%;min-height:80px;padding:8px;border:1px solid var(--vscode-input-border);border-radius:4px;background:var(--vscode-input-background);color:var(--vscode-input-foreground);resize:vertical;" \${isAnswered ? 'disabled' : ''}>\${savedText}</textarea>\`;
|
||||
} else {
|
||||
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 = \`
|
||||
\${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(() => {
|
||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||
if (submitBtn) {
|
||||
submitBtn.addEventListener('click', function() {
|
||||
const answers = {};
|
||||
questions.forEach((q, qIndex) => {
|
||||
const textarea = segmentDiv.querySelector(\`textarea[name="q\${qIndex}"]\`);
|
||||
if (textarea) {
|
||||
const value = textarea.value.trim();
|
||||
answers[qIndex] = value ? [value] : [];
|
||||
} else {
|
||||
const inputs = segmentDiv.querySelectorAll(\`input[name="q\${qIndex}"]:checked\`);
|
||||
answers[qIndex] = Array.from(inputs).map(input => input.value);
|
||||
}
|
||||
});
|
||||
handleMultiQuestionAnswer(segment.askId, answers, segmentDiv);
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
} else if (segment.type === 'agent') {
|
||||
renderAgentCard(segment, segmentDiv);
|
||||
} else if (segment.type === 'plan') {
|
||||
renderPlanCardInSegment(segment, segmentDiv, answeredQuestions);
|
||||
}
|
||||
|
||||
currentSegmentedMessage.appendChild(segmentDiv);
|
||||
});
|
||||
|
||||
if (isComplete) {
|
||||
currentSegmentedMessage = null;
|
||||
}
|
||||
|
||||
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function renderSegments(segments) {
|
||||
console.log('[WebView] renderSegments 被调用, segments:', segments);
|
||||
if (!segments || segments.length === 0) {
|
||||
console.log('[WebView] segments 为空,跳过渲染');
|
||||
return;
|
||||
}
|
||||
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();
|
||||
});
|
||||
updateSegmentsRealtime(segments, false);
|
||||
|
||||
// 历史消息渲染完成后添加操作按钮
|
||||
if (currentSegmentedMessage) {
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
copyBtn.onclick = () => {
|
||||
const textContent = segments.filter(s => s.type === 'text' && s.content).map(s => s.content).join('\\n');
|
||||
copyMessage(textContent, copyBtn);
|
||||
};
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'action-btn';
|
||||
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||
const dislikeBtn = document.createElement('button');
|
||||
dislikeBtn.className = 'action-btn';
|
||||
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
actionsDiv.appendChild(likeBtn);
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
currentSegmentedMessage.appendChild(actionsDiv);
|
||||
currentSegmentedMessage = null;
|
||||
}
|
||||
|
||||
smartScrollToBottom();
|
||||
}
|
||||
`;
|
||||
}
|
||||
56
src/views/textFormatter.ts
Normal file
56
src/views/textFormatter.ts
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 文本格式化模块
|
||||
* 功能:Markdown 文本转 HTML
|
||||
* 依赖:无
|
||||
* 使用场景:消息内容格式化显示
|
||||
*/
|
||||
|
||||
export function formatText(text: string): string {
|
||||
if (!text) return "";
|
||||
|
||||
let html = text;
|
||||
|
||||
const codeBlocks: string[] = [];
|
||||
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
|
||||
const language = lang || "plaintext";
|
||||
const escapedCode = code
|
||||
.trim()
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
const placeholder = `___CODE_BLOCK_${codeBlocks.length}___`;
|
||||
codeBlocks.push(`<pre><code class="language-${language}">${escapedCode}</code></pre>`);
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
const inlineCodes: string[] = [];
|
||||
html = html.replace(/`([^`]+)`/g, (match, code) => {
|
||||
const escapedCode = code.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const placeholder = `___INLINE_CODE_${inlineCodes.length}___`;
|
||||
inlineCodes.push(`<code>${escapedCode}</code>`);
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
html = html.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
|
||||
html = html.replace(/^### (.+)$/gm, "<h3>$1</h3>");
|
||||
html = html.replace(/^## (.+)$/gm, "<h2>$1</h2>");
|
||||
html = html.replace(/^# (.+)$/gm, "<h1>$1</h1>");
|
||||
html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
||||
html = html.replace(/\*(.+?)\*/g, "<em>$1</em>");
|
||||
html = html.replace(/^[\-\*] (.+)$/gm, "<li>$1</li>");
|
||||
html = html.replace(/(<li>.*<\/li>\n?)+/g, "<ul>$&</ul>");
|
||||
html = html.replace(/^\d+\. (.+)$/gm, "<li>$1</li>");
|
||||
html = html.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
html = html.replace(/\n/g, "<br>");
|
||||
|
||||
codeBlocks.forEach((block, index) => {
|
||||
html = html.replace(`___CODE_BLOCK_${index}___`, block);
|
||||
});
|
||||
|
||||
inlineCodes.forEach((code, index) => {
|
||||
html = html.replace(`___INLINE_CODE_${index}___`, code);
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
106
src/views/toolHelpers.ts
Normal file
106
src/views/toolHelpers.ts
Normal file
@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 工具辅助函数模块
|
||||
* 功能:工具图标、名称映射、VCD 路径解析
|
||||
* 依赖:toolIcons
|
||||
* 使用场景:工具调用显示
|
||||
*/
|
||||
|
||||
import {
|
||||
fileWriteIconSvg,
|
||||
fileReadIconSvg,
|
||||
fileDeleteIconSvg,
|
||||
syntaxCheckIconSvg,
|
||||
SearchCode,
|
||||
saveKnowledgeIconSvg,
|
||||
simulationIconSvg,
|
||||
waveformIconSvg,
|
||||
knowledgeLoadIconSvg,
|
||||
stateTransitionIconSvg,
|
||||
userQuestionIconSvg,
|
||||
updateStageIconSvg,
|
||||
successIconSvg,
|
||||
} from "../constants/toolIcons";
|
||||
|
||||
export function getToolIcon(toolName: string): string {
|
||||
const iconMap: Record<string, string> = {
|
||||
file_read: fileReadIconSvg,
|
||||
file_write: fileWriteIconSvg,
|
||||
file_delete: fileDeleteIconSvg,
|
||||
file_list: SearchCode,
|
||||
syntax_check: syntaxCheckIconSvg,
|
||||
simulation: simulationIconSvg,
|
||||
waveform_summary: waveformIconSvg,
|
||||
knowledge_save: saveKnowledgeIconSvg,
|
||||
knowledge_load: knowledgeLoadIconSvg,
|
||||
queryKnowledgeSummary: knowledgeLoadIconSvg,
|
||||
queryRules: knowledgeLoadIconSvg,
|
||||
setModule: fileWriteIconSvg,
|
||||
addSignal: fileWriteIconSvg,
|
||||
addSignalExample: fileWriteIconSvg,
|
||||
validateKnowledgeGraph: syntaxCheckIconSvg,
|
||||
querySignals: SearchCode,
|
||||
addPlan: fileWriteIconSvg,
|
||||
addEdge: fileWriteIconSvg,
|
||||
showPlan: SearchCode,
|
||||
addRule: fileWriteIconSvg,
|
||||
updateNode: fileWriteIconSvg,
|
||||
addStateTransition: stateTransitionIconSvg,
|
||||
askUser: userQuestionIconSvg,
|
||||
updatePhase: updateStageIconSvg,
|
||||
iverilog: successIconSvg,
|
||||
};
|
||||
return iconMap[toolName] || "";
|
||||
}
|
||||
|
||||
export function getToolDisplayName(toolName: string): string {
|
||||
const toolNameMap: Record<string, string> = {
|
||||
file_read: "已完成文件读取",
|
||||
file_write: "已完成文件写入",
|
||||
file_delete: "已完成文件删除",
|
||||
file_list: "已检索代码文件",
|
||||
syntax_check: "已完成语法检查",
|
||||
simulation: "已完成仿真",
|
||||
waveform_summary: "已完成波形分析",
|
||||
knowledge_save: "已保存知识库",
|
||||
knowledge_load: "已加载知识库",
|
||||
queryKnowledgeSummary: "已查询知识摘要",
|
||||
queryRules: "已查询规则",
|
||||
setModule: "已设置模块",
|
||||
addSignal: "信号分析完成",
|
||||
addSignalExample: "信号示例处理完成",
|
||||
validateKnowledgeGraph: "已验证知识图谱",
|
||||
querySignals: "已查询信号",
|
||||
addPlan: "已添加计划",
|
||||
addEdge: "已添加边",
|
||||
showPlan: "已显示计划",
|
||||
addRule: "已添加规则",
|
||||
updateNode: "已更新节点",
|
||||
addStateTransition: "已添加状态转换",
|
||||
spawnExplorer: "代码探索",
|
||||
spawnDebugger: "波形调试",
|
||||
askUser: "用户提问",
|
||||
updatePhase: "已更新阶段",
|
||||
iverilog: "已完成编译",
|
||||
};
|
||||
return toolNameMap[toolName] || toolName;
|
||||
}
|
||||
|
||||
export function parseMultiVcdPaths(toolResult: string): Array<{ name: string; path: string }> {
|
||||
if (!toolResult) return [];
|
||||
const result = String(toolResult);
|
||||
|
||||
const vcdListMatch = result.match(/VCD 文件列表:[\s\S]*?(?=\n\n|$)/);
|
||||
if (!vcdListMatch) return [];
|
||||
|
||||
const paths: Array<{ name: string; path: string }> = [];
|
||||
const lineRegex = /- (\w+): ([^\n]+)/g;
|
||||
let match;
|
||||
while ((match = lineRegex.exec(vcdListMatch[0])) !== null) {
|
||||
const name = match[1];
|
||||
const pathOrError = match[2].trim();
|
||||
if (!pathOrError.startsWith("失败")) {
|
||||
paths.push({ name: name + ".vcd", path: pathOrError });
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
@ -25,6 +25,7 @@ import {
|
||||
} from "./progressBar";
|
||||
import { getHighlightJsLinks } from "../components/codeHighlight";
|
||||
import { getCurrentEnv } from "../config/settings";
|
||||
import { taskCompleteIconSvg } from "../constants/toolIcons";
|
||||
import {
|
||||
getInvitationModalContent,
|
||||
getInvitationModalStyles,
|
||||
@ -304,7 +305,9 @@ export function getWebviewContent(
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
font-size:0.9rem
|
||||
font-size:0.9rem;
|
||||
color: var(--vscode-foreground);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-tool {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
@ -375,16 +378,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);
|
||||
@ -529,6 +548,9 @@ export function getWebviewContent(
|
||||
const modeSelect = document.getElementById('modeSelect');
|
||||
const messagesEl = document.getElementById('messages');
|
||||
|
||||
// 图标常量
|
||||
const taskCompleteIconSvg = ${JSON.stringify(taskCompleteIconSvg)};
|
||||
|
||||
// 全局变量
|
||||
let currentStreamingMessage = null;
|
||||
let loadingIndicator = null;
|
||||
@ -753,6 +775,45 @@ export function getWebviewContent(
|
||||
// 隐藏加载指示器
|
||||
hideLoadingIndicator();
|
||||
break;
|
||||
case 'taskComplete':
|
||||
// 显示任务完成提示
|
||||
const taskDiv = document.createElement('div');
|
||||
taskDiv.className = 'message bot-message';
|
||||
const taskActionsDiv = document.createElement('div');
|
||||
taskActionsDiv.className = 'message-actions';
|
||||
const taskMessageContent = document.createElement('span');
|
||||
taskMessageContent.innerHTML = taskCompleteIconSvg + ' 任务完成';
|
||||
const taskCopyBtn = document.createElement('button');
|
||||
taskCopyBtn.className = 'action-btn';
|
||||
taskCopyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
taskCopyBtn.onclick = () => {
|
||||
// 获取前一个 AI 消息的内容
|
||||
const prevMessage = taskDiv.previousElementSibling;
|
||||
if (prevMessage && prevMessage.classList.contains('bot-message')) {
|
||||
const textContent = prevMessage.textContent || '';
|
||||
copyMessage(textContent, taskCopyBtn);
|
||||
}
|
||||
};
|
||||
const taskLikeBtn = document.createElement('button');
|
||||
taskLikeBtn.className = 'action-btn';
|
||||
taskLikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
taskLikeBtn.onclick = () => toggleLike(taskLikeBtn);
|
||||
const taskDislikeBtn = document.createElement('button');
|
||||
taskDislikeBtn.className = 'action-btn';
|
||||
taskDislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
taskDislikeBtn.onclick = () => toggleDislike(taskDislikeBtn);
|
||||
taskActionsDiv.appendChild(taskMessageContent);
|
||||
taskActionsDiv.appendChild(taskCopyBtn);
|
||||
taskActionsDiv.appendChild(taskLikeBtn);
|
||||
taskActionsDiv.appendChild(taskDislikeBtn);
|
||||
taskDiv.appendChild(taskActionsDiv);
|
||||
messagesEl.appendChild(taskDiv);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
break;
|
||||
|
||||
case 'taskCompleteHistory':
|
||||
// 历史记录不显示任务完成提示
|
||||
break;
|
||||
|
||||
case 'workspaceStatus':
|
||||
// 更新工作区状态
|
||||
|
||||
Reference in New Issue
Block a user