13 Commits

Author SHA1 Message Date
eb345e3e1f feat:个人规则删除二次确认功能 2026-03-07 16:11:20 +08:00
8751944053 feat: 添加个人规则功能
- 新增个人规则管理模块 (personalRulesManager.ts)
   - 支持创建、编辑、删除多条规则
   - 规则存储在用户目录 ~/.iccoder/rules/
   - 对话时自动将规则传递给后端
   - 添加后端对接文档和 webpack 优化指南
2026-03-07 15:13:54 +08:00
06573e37d7 feat: 优化 webpack 打包配置
- 添加自动模式切换(开发/生产)
   - 启用 Tree Shaking 移除未使用代码
   - 加快编译速度(transpileOnly)
   - 添加打包体积监控
   - 自动清理旧文件
   - 添加打包优化文档
2026-03-06 18:27:56 +08:00
d740f4da44 feat: 支持文件路径标签带行号点击跳转
- 前端解析 file.v:1-2 格式,提取文件名和行号
   - 新增 openFilePathTag 命令,支持智能文件查找
   - 修复模板字符串中正则表达式转义问题
   - 不影响现有 openFile 和 diff 功能
2026-03-06 16:24:21 +08:00
f24bd38ec7 feat: 优化上下文项显示和识别逻辑
- 支持显示所有类型的上下文项(文件和代码片段)
   - 增强路径识别,支持代码片段格式(文件名:行号-行号)
2026-03-06 15:40:59 +08:00
45934baf0a feat: 添加上下文项点击功能
- 文件类型可点击打开文件
   - 代码片段可点击打开文件并选中对应代码
   - 文件夹类型不可点击
2026-03-06 10:13:27 +08:00
4384ee53c5 fix: 修复关闭面板后快捷键无法自动打开面板的问题
- 通过 try-catch 检测 webview 是否真正可用
   - 修复 panel._isDisposed 检测不准确的问题
   - 增加异常捕获防止发送消息时崩溃
   - 延长消息发送延迟至 500ms 确保面板加载完成
2026-03-06 10:05:52 +08:00
d89c326be5 Merge branch 'feat/DeleteConfirmation' into feat/codeToChat 2026-03-06 09:16:12 +08:00
2dccb4f871 update:changelog.md 2026-03-06 09:11:33 +08:00
a9ddf3074e 1.0.12 2026-03-06 09:10:51 +08:00
db087bb184 update:更新changelog.md 2026-03-06 09:10:09 +08:00
5e9083041f fix: 修复多选问题提交后选中项不显示高亮的问题 2026-03-06 09:08:38 +08:00
be0555d6bc feat:codeToChat 2026-03-06 08:59:02 +08:00
20 changed files with 1627 additions and 135 deletions

View File

@ -2,6 +2,12 @@
所有重要的项目变更都将记录在此文件中。
## [1.0.12] - 2026-03-06
### 新增
- 支持 AskUserQuestion 多问题和多选功能
## [1.0.9] - 2026-03-04
### 优化

View File

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

View File

@ -0,0 +1,247 @@
# 个人规则功能 - 后端对接文档
## 1. 功能概述
个人规则功能允许用户创建多条自定义规则,这些规则会在每次对话时自动传递给后端,由后端注入到 AI 的系统提示词中,从而影响 AI 的回答风格和行为。
## 2. 前端实现说明
### 2.1 用户界面
- 用户可以在设置页面创建、修改、删除多条规则
- 每条规则包含:规则名称 + 规则内容
- 全局开关:启用/禁用所有规则
### 2.2 规则存储
- 存储位置:`C:\Users\{用户名}\.iccoder\rules\`
- 文件格式:每条规则一个独立的 `.md` 文件
- 文件命名:`rule-{时间戳}.md`
- 文件内容格式:
```markdown
# 规则名称
规则内容详细描述...
```
### 2.3 规则传输逻辑
- **开关开启**:所有规则内容合并后通过 `personalRules` 字段传给后端
- **开关关闭**`personalRules` 字段为 `undefined`,不传给后端
## 3. 后端接口变更
### 3.1 DialogRequest 接口新增字段
在现有的 `DialogRequest` 接口中新增 `personalRules` 字段:
```typescript
export interface DialogRequest {
taskId: string;
message: string;
userId: string;
mode: RunMode;
serviceTier?: ServiceTier;
token?: string;
compactedData?: CompactedMemory;
newMessages?: CompactedMessage[];
knowledgeData?: string;
personalRules?: string; // 新增:个人规则内容
}
```
### 3.2 字段说明
| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `personalRules` | `string` | 否 | 用户的个人规则内容,多条规则用 `\n\n` 分隔 |
### 3.3 字段示例
**单条规则:**
```json
{
"message": "帮我写一个排序函数",
"personalRules": "始终使用中文回复,代码注释要详细"
}
```
**多条规则(合并后):**
```json
{
"message": "帮我写一个排序函数",
"personalRules": "始终使用中文回复,代码注释要详细\n\n使用 TypeScript 严格模式\n\n遵循项目编码规范"
}
```
**规则关闭:**
```json
{
"message": "帮我写一个排序函数",
"personalRules": undefined
}
```
## 4. 后端处理要求
### 4.1 接收处理
```typescript
// 伪代码示例
function handleDialogRequest(request: DialogRequest) {
const { message, personalRules, ...otherFields } = request;
// 检查是否有个人规则
if (personalRules && personalRules.trim()) {
// 有规则:注入到系统提示词
return processWithRules(message, personalRules, otherFields);
} else {
// 无规则:正常处理
return processNormal(message, otherFields);
}
}
```
### 4.2 规则注入策略
**重要:规则必须注入到系统提示词层,而不是用户消息层**
推荐的注入顺序(优先级从高到低):
1. **平台安全策略**(最高优先级,不可被覆盖)
2. **产品默认系统提示**
3. **用户个人规则** ← 在这里注入
4. **用户输入消息**
### 4.3 注入示例
```typescript
// 伪代码示例
function buildSystemPrompt(personalRules?: string): string {
let systemPrompt = `
你是一个专业的 AI 助手。
遵循以下基本原则:
- 安全第一
- 准确回答
- 友好交流
`;
// 如果有个人规则,追加到系统提示词
if (personalRules && personalRules.trim()) {
systemPrompt += `\n\n用户的个人偏好和规则\n${personalRules}`;
}
return systemPrompt;
}
function processWithRules(
userMessage: string,
personalRules: string,
otherFields: any
) {
const systemPrompt = buildSystemPrompt(personalRules);
// 调用 AI 模型
return callAIModel({
system: systemPrompt,
user: userMessage,
...otherFields
});
}
```
## 5. 注意事项
### 5.1 安全性
- ⚠️ **个人规则不能覆盖平台安全策略**
- ⚠️ **需要对规则内容进行基本的安全检查**
- ⚠️ **防止注入攻击(如提示词注入)**
### 5.2 长度限制
- 前端已限制单条规则内容,但多条规则合并后可能较长
- 建议后端设置总长度上限(如 10000 字符)
- 超限时可以截断或返回错误提示
### 5.3 兼容性
- `personalRules` 字段为可选字段
- 旧版本前端不传此字段时,后端应正常处理(向后兼容)
- 字段为 `undefined` 或空字符串时,视为无规则
### 5.4 日志记录
建议在日志中记录:
- 本次请求是否包含个人规则
- 规则内容的长度(不要记录完整内容,避免隐私泄露)
- 规则注入是否成功
示例日志:
```
[INFO] Dialog request received
- taskId: abc123
- userId: user456
- hasPersonalRules: true
- rulesLength: 156
- rulesInjected: success
```
## 6. 测试建议
### 6.1 功能测试
1. **无规则场景**`personalRules` 为 `undefined`,正常对话
2. **单条规则**:传入一条规则,验证 AI 是否遵循
3. **多条规则**:传入多条规则,验证 AI 是否同时遵循
4. **规则冲突**:传入相互矛盾的规则,观察 AI 行为
5. **超长规则**:传入超长内容,验证截断或错误处理
### 6.2 安全测试
1. **提示词注入**:尝试在规则中注入恶意提示词
2. **覆盖安全策略**:尝试用规则覆盖平台安全限制
3. **特殊字符**:测试规则中包含特殊字符的情况
### 6.3 性能测试
1. **大量规则**:测试 10+ 条规则的性能影响
2. **高频请求**:测试规则注入对响应时间的影响
## 7. 错误处理
### 7.1 可能的错误场景
| 错误场景 | 处理方式 |
|---------|---------|
| 规则内容为空字符串 | 视为无规则,正常处理 |
| 规则内容超长 | 截断或返回错误 |
| 规则包含非法内容 | 过滤或拒绝请求 |
| 规则注入失败 | 降级为无规则对话 |
### 7.2 错误响应示例
```json
{
"error": {
"code": "RULES_TOO_LONG",
"message": "个人规则内容超过长度限制(最大 10000 字符)"
}
}
```
## 8. 验收标准
### 8.1 基本功能
- [ ] 能正确接收 `personalRules` 字段
- [ ] 规则能正确注入到系统提示词
- [ ] 规则关闭时不影响正常对话
- [ ] 多条规则能同时生效
### 8.2 安全性
- [ ] 规则不能覆盖平台安全策略
- [ ] 有基本的内容安全检查
- [ ] 日志中不记录完整规则内容
### 8.3 兼容性
- [ ] 旧版本前端(无此字段)能正常工作
- [ ] 字段为 `undefined` 时正常处理
## 9. 联系方式
如有疑问,请联系前端开发团队。
---
**文档版本**v1.0
**最后更新**2026-03-07

View File

@ -0,0 +1,379 @@
# Webpack 打包优化完整教程
## 目录
1. [优化前的问题](#优化前的问题)
2. [优化方案详解](#优化方案详解)
3. [配置对比](#配置对比)
4. [使用指南](#使用指南)
5. [效果验证](#效果验证)
---
## 优化前的问题
### 原始配置存在的问题
```javascript
// ❌ 问题1固定使用 none 模式
mode: 'none'
// 导致:生产环境代码不压缩,体积大
// ❌ 问题2没有 Tree Shaking
// 导致:未使用的代码也被打包
// ❌ 问题3ts-loader 默认配置
loader: 'ts-loader'
// 导致:每次编译都做类型检查,速度慢
// ❌ 问题4没有性能监控
// 导致:打包体积过大时不知道
```
---
## 优化方案详解
### 1. 自动模式切换
**原理**:根据环境变量自动选择打包模式
```javascript
// 优化前
mode: 'none'
// 优化后
mode: process.env.NODE_ENV === 'production' ? 'production' : 'none'
```
**效果**
- 开发模式:代码可读,方便调试
- 生产模式:自动压缩,体积减小 40-60%
---
### 2. Tree Shaking摇树优化
**原理**:移除未使用的代码
```javascript
optimization: {
minimize: process.env.NODE_ENV === 'production',
usedExports: true // 标记未使用的导出
}
```
**示例**
```javascript
// utils.ts
export function usedFunc() { }
export function unusedFunc() { } // 不会被打包
// main.ts
import { usedFunc } from './utils';
```
**效果**:减少 10-30% 体积
---
### 3. 加快编译速度
**原理**:跳过类型检查,只做转译
```javascript
{
loader: 'ts-loader',
options: {
transpileOnly: true, // 跳过类型检查
compilerOptions: {
sourceMap: true
}
}
}
```
**说明**
- 类型检查交给 IDE 和 CI
- 编译速度提升 50-70%
---
### 4. 自动清理旧文件
```javascript
output: {
clean: true // 每次打包前清空 dist 目录
}
```
**效果**:避免旧文件残留
---
### 5. 性能监控
```javascript
performance: {
hints: 'warning',
maxAssetSize: 2 * 1024 * 1024, // 2MB
maxEntrypointSize: 2 * 1024 * 1024
}
```
**效果**:超过 2MB 会警告
---
### 6. Source Map 优化
```javascript
devtool: process.env.NODE_ENV === 'production'
? 'hidden-source-map' // 生产:隐藏源码
: 'nosources-source-map' // 开发:保留调试信息
```
---
### 7. 模块解析优化
```javascript
resolve: {
extensions: ['.ts', '.js'],
mainFields: ['module', 'main'] // 优先使用 ES 模块
}
```
**效果**:更好的 Tree Shaking 效果
---
## 配置对比
### 优化前
```javascript
const extensionConfig = {
target: 'node',
mode: 'none', // 固定模式
entry: './src/extension.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2'
// 没有 clean
},
module: {
rules: [{
test: /\.ts$/,
use: [{ loader: 'ts-loader' }] // 默认配置
}]
},
devtool: 'nosources-source-map' // 固定
// 没有 optimization
// 没有 performance
};
```
### 优化后
```javascript
const extensionConfig = {
target: 'node',
mode: process.env.NODE_ENV === 'production' ? 'production' : 'none',
entry: './src/extension.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2',
clean: true // ✅ 自动清理
},
resolve: {
extensions: ['.ts', '.js'],
mainFields: ['module', 'main'] // ✅ 优化解析
},
module: {
rules: [{
test: /\.ts$/,
use: [{
loader: 'ts-loader',
options: {
transpileOnly: true, // ✅ 加速编译
compilerOptions: { sourceMap: true }
}
}]
}]
},
devtool: process.env.NODE_ENV === 'production'
? 'hidden-source-map'
: 'nosources-source-map',
optimization: {
minimize: process.env.NODE_ENV === 'production',
usedExports: true // ✅ Tree Shaking
},
performance: {
hints: 'warning',
maxAssetSize: 2 * 1024 * 1024,
maxEntrypointSize: 2 * 1024 * 1024
}
};
```
---
## 使用指南
### 开发模式
```bash
# 单次编译
pnpm run compile
# 监听模式(推荐)
pnpm run watch
```
**特点**
- 不压缩代码
- 快速编译
- 保留调试信息
---
### 生产模式
#### Windows
```bash
set NODE_ENV=production && pnpm run package
```
#### macOS/Linux
```bash
NODE_ENV=production pnpm run package
```
**特点**
- 代码压缩
- Tree Shaking
- 隐藏源码
---
### 一键打包 VSIX
```bash
# Windows
set NODE_ENV=production && pnpm run package && npx vsce package
# macOS/Linux
NODE_ENV=production pnpm run package && npx vsce package
```
---
## 效果验证
### 1. 查看打包体积
```bash
# Windows
dir dist\extension.js
# macOS/Linux
ls -lh dist/extension.js
```
### 2. 对比测试
| 模式 | 体积 | 编译时间 | 可读性 |
|------|------|----------|--------|
| 开发模式 | ~800KB | 5s | 高 |
| 生产模式 | ~400KB | 8s | 低(压缩) |
### 3. 性能警告
如果看到这个警告:
```
WARNING in asset size limit: The following asset(s) exceed the recommended size limit (2 MiB).
```
**解决方案**
1. 检查是否引入了不必要的依赖
2. 将大型库添加到 `externals`
3. 考虑代码分割
---
## 常见问题
### Q1: 为什么开发模式不压缩?
**A**: 保持代码可读性,方便调试和查看错误堆栈。
### Q2: transpileOnly 会影响类型安全吗?
**A**: 不会。IDE 和 `tsc --noEmit` 仍会做类型检查。
### Q3: 如何查看 Tree Shaking 效果?
**A**: 使用 `webpack-bundle-analyzer`
```bash
pnpm add -D webpack-bundle-analyzer
```
### Q4: 生产模式编译失败怎么办?
**A**: 先用开发模式确认代码无误,再切换生产模式。
---
## 进阶优化(可选)
### 1. 排除更多依赖
```javascript
externals: {
vscode: 'commonjs vscode',
'node-notifier': 'commonjs node-notifier',
// 如果这些库很大,可以排除
'vcdrom': 'commonjs vcdrom',
'@wavedrom/doppler': 'commonjs @wavedrom/doppler'
}
```
### 2. 代码分割
```javascript
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
}
}
}
}
```
### 3. 缓存优化
```javascript
{
loader: 'ts-loader',
options: {
transpileOnly: true,
experimentalWatchApi: true // 监听模式优化
}
}
```
---
## 总结
通过这些优化:
- ✅ 生产体积减少 40-60%
- ✅ 编译速度提升 50-70%
- ✅ 自动清理和监控
- ✅ 更好的开发体验
**推荐工作流**
1. 开发时用 `pnpm run watch`
2. 提交前用 `pnpm run compile` 检查
3. 发布前用生产模式打包

View File

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

View File

@ -2,7 +2,7 @@
"name": "iccoder",
"displayName": "IC Coder: Agentic Verilog Platform",
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
"version": "1.0.11",
"version": "1.0.12",
"publisher": "ICCoderAgenticVerilogPlatform",
"engines": {
"vscode": "^1.80.0"
@ -54,6 +54,28 @@
"command": "ic-coder.testNotification",
"title": "测试系统通知",
"category": "IC Coder"
},
{
"command": "ic-coder.addCodeToChat",
"title": "添加到 IC Coder 对话",
"category": "IC Coder"
}
],
"menus": {
"editor/context": [
{
"command": "ic-coder.addCodeToChat",
"when": "editorHasSelection",
"group": "9_cutcopypaste"
}
]
},
"keybindings": [
{
"command": "ic-coder.addCodeToChat",
"key": "ctrl+l",
"mac": "cmd+l",
"when": "editorTextFocus && editorHasSelection"
}
],
"viewsContainers": {
@ -95,6 +117,11 @@
"configuration": {
"title": "IC Coder",
"properties": {
"ic-coder.personalRulesEnabled": {
"type": "boolean",
"default": true,
"description": "启用个人规则"
},
"ic-coder.enableSystemNotification": {
"type": "boolean",
"default": true,

View File

@ -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 peopleRules = `<svg t="1772851533961" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11188" width="16" height="16"><path d="M652.8 534.4c70.4-44.8 115.2-124.8 115.2-214.4 0-140.8-115.2-256-256-256s-256 115.2-256 256c0 89.6 44.8 169.6 115.2 214.4C192 592 64 761.6 64 960h64c0-211.2 172.8-384 384-384s384 172.8 384 384h64c0-198.4-128-368-307.2-425.6zM512 512c-105.6 0-192-86.4-192-192s86.4-192 192-192 192 86.4 192 192-86.4 192-192 192z" fill="#6caed4" p-id="11189"></path></svg>`;

View File

@ -10,10 +10,42 @@ import { initCreditsService } from "./services/creditsService";
import { isTokenExpired } from "./utils/jwtUtils";
import { NotificationService } from "./services/notificationService";
import { InvitationService } from "./services/invitationService";
import { ICCoderCodeActionProvider } from "./providers/codeActionProvider";
export async function activate(context: vscode.ExtensionContext) {
console.log("🎉 IC Coder 插件已激活!");
// 创建装饰类型(代码旁边的提示)
const decorationType = vscode.window.createTextEditorDecorationType({
after: {
contentText: ' Ctrl+L 添加到 IC Coder 对话',
color: '#888',
fontStyle: 'italic',
margin: '0 0 0 1em'
}
});
// 更新装饰
const updateDecorations = () => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
if (!editor.selection.isEmpty) {
const range = new vscode.Range(editor.selection.end, editor.selection.end);
const decoration = { range };
editor.setDecorations(decorationType, [decoration]);
} else {
editor.setDecorations(decorationType, []);
}
};
context.subscriptions.push(
vscode.window.onDidChangeTextEditorSelection(updateDecorations),
vscode.window.onDidChangeActiveTextEditor(updateDecorations)
);
updateDecorations();
// 初始化通知服务
const notificationService = NotificationService.getInstance(context);
console.log('[Extension] 通知服务已初始化');
@ -250,6 +282,81 @@ export async function activate(context: vscode.ExtensionContext) {
}
);
// 注册命令:将选中代码添加到对话
const addCodeToChat = vscode.commands.registerCommand(
"ic-coder.addCodeToChat",
async () => {
console.log('[addCodeToChat] 命令触发');
const editor = vscode.window.activeTextEditor;
if (!editor) {
console.log('[addCodeToChat] 没有活动编辑器');
return;
}
const selection = editor.selection;
const selectedText = editor.document.getText(selection);
if (!selectedText) {
vscode.window.showWarningMessage("请先选择代码");
return;
}
const fileName = editor.document.fileName;
const startLine = selection.start.line + 1;
const endLine = selection.end.line + 1;
// 检查是否已有打开的面板
let panel = (global as any).currentICHelperPanel;
let needCreatePanel = false;
if (!panel) {
needCreatePanel = true;
} else {
// 尝试访问 webview如果抛出异常说明已销毁
try {
const _ = panel.webview;
} catch (e) {
needCreatePanel = true;
}
}
console.log('[addCodeToChat] 需要创建面板:', needCreatePanel);
if (needCreatePanel) {
console.log('[addCodeToChat] 正在打开面板...');
await showICHelperPanel(context);
panel = (global as any).currentICHelperPanel;
console.log('[addCodeToChat] 面板打开后状态:', panel ? '成功' : '失败');
// 如果面板仍未创建(如未登录),直接返回
if (!panel) {
console.log('[addCodeToChat] 面板创建失败,退出');
return;
}
}
// 发送代码上下文
console.log('[addCodeToChat] 准备发送代码到面板');
setTimeout(() => {
try {
if (panel?.webview) {
console.log('[addCodeToChat] 发送 addCodeContext 消息');
panel.webview.postMessage({
command: 'addCodeContext',
fileName,
startLine,
endLine,
code: selectedText,
languageId: editor.document.languageId
});
}
} catch (e) {
console.log('[addCodeToChat] 发送消息失败:', e);
}
}, 500);
}
);
// 注册命令:查看会话历史
// TODO: 这些命令需要根据新的任务架构重新实现
// 暂时注释掉,等待重新实现
@ -312,6 +419,13 @@ export async function activate(context: vscode.ExtensionContext) {
// 注册 VCD 自定义编辑器
const vcdEditorProvider = VCDViewerEditorProvider.register(context, vcdFileServer);
// 注册 Code Action Provider
const codeActionProvider = vscode.languages.registerCodeActionsProvider(
{ scheme: 'file' },
new ICCoderCodeActionProvider(),
{ providedCodeActionKinds: [vscode.CodeActionKind.RefactorRewrite] }
);
// 添加到订阅
context.subscriptions.push(
openPanelCommand,
@ -322,6 +436,7 @@ export async function activate(context: vscode.ExtensionContext) {
logoutCommand,
changeInvitationCodeCommand,
testNotificationCommand,
addCodeToChat,
// testTrialUserCommand,
// testExpiredUserCommand,
// TODO: 等待重新实现这些命令
@ -332,7 +447,8 @@ export async function activate(context: vscode.ExtensionContext) {
// clearHistoryCommand,
// searchSessionCommand,
viewRegistration,
vcdEditorProvider
vcdEditorProvider,
codeActionProvider
);
}

View File

@ -25,6 +25,7 @@ import { MessageType } from "../types/chatHistory";
import { getCachedUserInfo } from "../services/userService";
import { isTokenExpired } from "../utils/jwtUtils";
import { setBalanceUpdateCallback } from "../services/creditsService";
import { savePersonalRule, updatePersonalRule, deletePersonalRule, loadPersonalRules } from "../utils/personalRulesManager";
/**
* 获取会员等级图标 URI
@ -143,6 +144,9 @@ export async function showICHelperPanel(
},
);
// 保存 panel 引用到全局
(global as any).currentICHelperPanel = panel;
// 为面板生成唯一ID
const panelId = `panel_${Date.now()}_${Math.random()
.toString(36)
@ -491,6 +495,125 @@ export async function showICHelperPanel(
// 退出登录
vscode.commands.executeCommand("ic-coder.logout");
break;
case "savePersonalRule":
// 保存个人规则
if (message.name && message.content && message.enabled !== undefined) {
const success = await savePersonalRule(message.name, message.content, message.enabled);
if (success) {
const rulesData = loadPersonalRules();
panel.webview.postMessage({
command: "personalRulesLoaded",
data: rulesData
});
}
}
break;
case "updatePersonalRule":
// 更新个人规则
if (message.filename && message.name && message.content && message.enabled !== undefined) {
const success = await updatePersonalRule(message.filename, message.name, message.content, message.enabled);
if (success) {
const rulesData = loadPersonalRules();
panel.webview.postMessage({
command: "personalRulesLoaded",
data: rulesData
});
}
}
break;
case "deletePersonalRule":
// 删除个人规则
if (message.filename) {
const success = await deletePersonalRule(message.filename);
if (success) {
const rulesData = loadPersonalRules();
panel.webview.postMessage({
command: "personalRulesLoaded",
data: rulesData
});
}
}
break;
case "loadPersonalRules":
// 加载个人规则
const rulesData = loadPersonalRules();
panel.webview.postMessage({
command: "personalRulesLoaded",
data: rulesData
});
break;
case "openFile":
// 打开文件
if (message.filePath) {
const path = require('path');
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const fullPath = path.isAbsolute(message.filePath) || !workspaceFolder
? message.filePath
: vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath;
vscode.workspace.openTextDocument(fullPath).then(doc => {
vscode.window.showTextDocument(doc);
});
}
break;
case "openFileWithSelection":
// 打开文件并选中代码
if (message.filePath) {
const path = require('path');
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const fullPath = path.isAbsolute(message.filePath) || !workspaceFolder
? message.filePath
: vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath;
vscode.workspace.openTextDocument(fullPath).then(doc => {
vscode.window.showTextDocument(doc).then(editor => {
const start = new vscode.Position(message.startLine - 1, 0);
const end = new vscode.Position(message.endLine - 1, doc.lineAt(message.endLine - 1).text.length);
editor.selection = new vscode.Selection(start, end);
editor.revealRange(new vscode.Range(start, end));
});
});
}
break;
case "openFilePathTag":
// 打开文件路径标签(智能查找)
if (message.filePath) {
const path = require('path');
const fs = require('fs');
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
let fullPath = message.filePath;
// 如果是相对路径且工作区存在
if (!path.isAbsolute(message.filePath) && workspaceFolder) {
const candidatePath = vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath;
// 检查文件是否存在
if (fs.existsSync(candidatePath)) {
fullPath = candidatePath;
} else {
// 尝试在工作区中搜索该文件
const fileName = path.basename(message.filePath);
const files = await vscode.workspace.findFiles(`**/${fileName}`, '**/node_modules/**', 1);
if (files.length > 0) {
fullPath = files[0].fsPath;
}
}
}
if (message.startLine && message.endLine) {
vscode.workspace.openTextDocument(fullPath).then(doc => {
vscode.window.showTextDocument(doc).then(editor => {
const start = new vscode.Position(message.startLine - 1, 0);
const end = new vscode.Position(message.endLine - 1, doc.lineAt(message.endLine - 1).text.length);
editor.selection = new vscode.Selection(start, end);
editor.revealRange(new vscode.Range(start, end));
});
});
} else {
vscode.workspace.openTextDocument(fullPath).then(doc => {
vscode.window.showTextDocument(doc);
});
}
}
break;
case "acceptChange":
// 采纳变更
if (message.changeId) {

View File

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

View File

@ -28,6 +28,7 @@ import type {
PlanConfirmEvent,
} from "../types/api";
import { submitToolConfirm, submitAnswer, stopDialog } from "./apiClient";
import { getActiveRules } from "../utils/personalRulesManager";
import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { getUserIdFromToken, isTokenExpired } from "../utils/jwtUtils";
import { updateCachedBalance } from "./creditsService";
@ -502,6 +503,7 @@ export class DialogSession {
compactedData: compactedData || undefined,
newMessages: newMessages.length > 0 ? newMessages : undefined,
knowledgeData: knowledgeData || undefined,
personalRules: getActiveRules() || undefined,
};
// 追踪用户消息

View File

@ -48,6 +48,8 @@ export interface DialogRequest {
newMessages?: CompactedMessage[];
/** 知识图谱数据JSON 字符串,用于恢复知识图谱) */
knowledgeData?: string;
/** 个人规则 */
personalRules?: string;
}
// ============== SSE 事件类型 ==============

View File

@ -0,0 +1,147 @@
/**
* 个人规则管理工具
* 功能:读写个人规则文件
* 依赖vscode, fs, path
* 使用场景:保存和加载用户的个人规则
*/
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
/**
* 获取规则目录路径
*/
function getRulesDir(): string {
return path.join(os.homedir(), '.iccoder', 'rules');
}
/**
* 确保规则目录存在
*/
function ensureRulesDir(): void {
const dir = getRulesDir();
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
/**
* 从文件内容中提取规则名称
*/
function extractRuleName(content: string): string {
const lines = content.split('\n');
const firstLine = lines[0]?.trim();
if (firstLine && firstLine.startsWith('# ')) {
return firstLine.substring(2).trim();
}
return content.substring(0, 30) + (content.length > 30 ? '...' : '');
}
/**
* 保存新规则
*/
export async function savePersonalRule(name: string, content: string, enabled: boolean): Promise<boolean> {
try {
ensureRulesDir();
const timestamp = Date.now();
const filename = `rule-${timestamp}.md`;
const filePath = path.join(getRulesDir(), filename);
const fileContent = `# ${name}\n\n${content}`;
fs.writeFileSync(filePath, fileContent, 'utf-8');
await vscode.workspace.getConfiguration('ic-coder').update('personalRulesEnabled', enabled, vscode.ConfigurationTarget.Global);
vscode.window.showInformationMessage('规则已保存');
return true;
} catch (error) {
vscode.window.showErrorMessage(`保存规则失败: ${error}`);
return false;
}
}
/**
* 更新规则
*/
export async function updatePersonalRule(filename: string, name: string, content: string, enabled: boolean): Promise<boolean> {
try {
const filePath = path.join(getRulesDir(), filename);
const fileContent = `# ${name}\n\n${content}`;
fs.writeFileSync(filePath, fileContent, 'utf-8');
await vscode.workspace.getConfiguration('ic-coder').update('personalRulesEnabled', enabled, vscode.ConfigurationTarget.Global);
vscode.window.showInformationMessage('规则已更新');
return true;
} catch (error) {
vscode.window.showErrorMessage(`更新规则失败: ${error}`);
return false;
}
}
/**
* 删除规则
*/
export async function deletePersonalRule(filename: string): Promise<boolean> {
try {
const filePath = path.join(getRulesDir(), filename);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
vscode.window.showInformationMessage('规则已删除');
return true;
}
return false;
} catch (error) {
vscode.window.showErrorMessage(`删除规则失败: ${error}`);
return false;
}
}
/**
* 加载所有规则
*/
export function loadPersonalRules(): { rules: Array<{ filename: string; name: string; content: string }>; enabled: boolean } {
const enabled = vscode.workspace.getConfiguration('ic-coder').get<boolean>('personalRulesEnabled', true);
const dir = getRulesDir();
if (!fs.existsSync(dir)) {
return { rules: [], enabled };
}
try {
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
const rules = files.map(filename => {
const content = fs.readFileSync(path.join(dir, filename), 'utf-8');
const lines = content.split('\n');
let name = '';
let actualContent = content;
if (lines[0]?.trim().startsWith('# ')) {
name = lines[0].substring(2).trim();
actualContent = lines.slice(2).join('\n').trim();
} else {
name = extractRuleName(content);
}
return { filename, name, content: actualContent };
});
return { rules, enabled };
} catch (error) {
console.error('读取规则失败:', error);
return { rules: [], enabled };
}
}
/**
* 获取当前生效的所有规则内容
*/
export function getActiveRules(): string | null {
const { rules, enabled } = loadPersonalRules();
if (!enabled || rules.length === 0) {
return null;
}
return rules.map(r => r.content).join('\n\n');
}

View File

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

View File

@ -46,10 +46,21 @@ export function getFilePathTagScript(): string {
return `
// 处理文件路径标签点击
function handleFilePathClick(filePath) {
vscode.postMessage({
command: 'openFile',
filePath: filePath
});
// 解析文件路径,支持 file.v:5-8 格式
const match = filePath.match(/^(.+?):(\\d+)-(\\d+)$/);
if (match) {
vscode.postMessage({
command: 'openFilePathTag',
filePath: match[1],
startLine: parseInt(match[2]),
endLine: parseInt(match[3])
});
} else {
vscode.postMessage({
command: 'openFilePathTag',
filePath: filePath
});
}
}
// 创建文件路径标签

View File

@ -432,15 +432,14 @@ export function getInputAreaScript(): string {
// 获取上下文项
const contextItems = window.getContextItems ? window.getContextItems() : [];
// 构建显示消息:如果有上下文文件,添加文件路径前缀
// 构建显示消息:如果有上下文,添加路径前缀
let displayText = text;
if (contextItems.length > 0) {
const filePaths = contextItems
.filter(item => item.type === 'file')
const contextPaths = contextItems
.map(item => item.displayPath || item.path)
.join(' ');
if (filePaths) {
displayText = filePaths + ' ' + text;
if (contextPaths) {
displayText = contextPaths + ' ' + text;
}
}

View File

@ -863,8 +863,8 @@ export function getMessageAreaScript(): string {
const textParts = [];
parts.forEach(part => {
// 判断是否为文件路径:包含路径分隔符文件扩展名
if (part.includes('/') || part.includes('\\\\') || /\\.[a-zA-Z0-9]+$/.test(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);
@ -1211,7 +1211,7 @@ export function getMessageAreaScript(): string {
const optionsHtml = q.options.map(opt => {
const isSelected = selectedAnswers.includes(opt);
return \`<label style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:4px 0;">
return \`<label class="question-option\${isSelected ? ' selected' : ''}" style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:4px 0;">
<input type="\${inputType}" name="\${inputName}" value="\${opt}" \${isSelected ? 'checked' : ''} \${isAnswered ? 'disabled' : ''}>
<span>\${opt}</span>
</label>\`;
@ -1729,9 +1729,18 @@ export function getMessageAreaScript(): string {
// 标记问题已回答
segmentDiv.classList.add('answered');
// 禁用所有输入
// 禁用所有输入并保持选中状态的高亮
const inputs = segmentDiv.querySelectorAll('input');
inputs.forEach(input => input.disabled = true);
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');

View File

@ -1,77 +1,67 @@
import { peopleRules } from "../constants/toolIcons";
/**
* 获取规则设置组件的 HTML 内容
*/
export function getRulesSettingsComponentContent(): string {
return `
<div class="rules-settings">
<h3 class="settings-section-title">规则设置</h3>
<div class="rules-header">
<h3 class="settings-section-title">个人规则</h3>
<button class="add-rule-button" onclick="showAddRuleModal()">+ 创建</button>
</div>
<div class="settings-section">
<div class="settings-item">
<div class="settings-item-header">
<label class="settings-item-label">启用自定义规则</label>
<span class="settings-item-description">使用自定义规则来控制 AI 行为</span>
<label class="settings-item-label">启用个人规则</label>
<span class="settings-item-description">规则将在每次对话时自动应用</span>
</div>
<label class="settings-switch">
<input type="checkbox" id="enableCustomRulesCheckbox" checked>
<input type="checkbox" id="enablePersonalRulesCheckbox" checked>
<span class="settings-switch-slider"></span>
</label>
</div>
</div>
<div class="settings-section">
<h4 class="settings-subsection-title">系统规则</h4>
<div class="rules-textarea-container">
<div class="rules-list" id="rulesList">
<!-- 规则列表将动态插入这里 -->
</div>
<!-- 添加/编辑规则弹窗 -->
<div class="rule-modal" id="ruleModal" style="display: none;">
<div class="rule-modal-content">
<h4 id="modalTitle">创建个人规则</h4>
<input
type="text"
class="rule-name-input"
id="ruleNameInput"
placeholder="规则名称"
/>
<textarea
class="rules-textarea"
id="systemRulesTextarea"
placeholder="在此输入系统规则,例如:&#10;- 始终使用中文回复&#10;- 代码注释要详细&#10;- 遵循项目编码规范"
rows="8"
class="rule-textarea"
id="ruleTextarea"
placeholder="输入规则内容..."
rows="10"
></textarea>
<div class="rules-textarea-hint">
系统规则会在每次对话开始时应用
<div class="rule-modal-actions">
<button class="settings-button settings-button-primary" onclick="saveRule()">保存</button>
<button class="settings-button settings-button-secondary" onclick="closeRuleModal()">取消</button>
</div>
</div>
</div>
<div class="settings-section">
<h4 class="settings-subsection-title">代码生成规则</h4>
<div class="rules-textarea-container">
<textarea
class="rules-textarea"
id="codeRulesTextarea"
placeholder="在此输入代码生成规则,例如:&#10;- 使用 TypeScript 严格模式&#10;- 函数命名使用驼峰命名法&#10;- 添加必要的错误处理"
rows="8"
></textarea>
<div class="rules-textarea-hint">
这些规则会在生成代码时应用
<!-- 删除确认弹窗 -->
<div class="rule-modal" id="deleteConfirmModal" style="display: none;">
<div class="rule-modal-content" style="width: 400px;">
<h4>确认删除</h4>
<p id="deleteConfirmText" style="color: var(--vscode-foreground); margin: 16px 0;"></p>
<div class="rule-modal-actions">
<button class="settings-button settings-button-primary" onclick="confirmDelete()">确定</button>
<button class="settings-button settings-button-secondary" onclick="closeDeleteConfirmModal()">取消</button>
</div>
</div>
</div>
<div class="settings-section">
<h4 class="settings-subsection-title">Verilog 规则</h4>
<div class="rules-textarea-container">
<textarea
class="rules-textarea"
id="verilogRulesTextarea"
placeholder="在此输入 Verilog 代码规则,例如:&#10;- 使用非阻塞赋值 (<=) 在时序逻辑中&#10;- 模块命名使用小写加下划线&#10;- 添加详细的端口注释"
rows="8"
></textarea>
<div class="rules-textarea-hint">
这些规则会在生成 Verilog 代码时应用
</div>
</div>
</div>
<div class="settings-actions">
<button class="settings-button settings-button-primary" onclick="saveRulesSettings()">
保存规则
</button>
<button class="settings-button settings-button-secondary" onclick="resetRulesSettings()">
重置为默认
</button>
</div>
</div>
`;
}
@ -85,11 +75,144 @@ export function getRulesSettingsComponentStyles(): string {
max-width: 700px;
}
.rules-textarea-container {
margin-top: 8px;
.rules-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.rules-textarea {
.add-rule-button {
padding: 6px 12px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.add-rule-button:hover {
background: var(--vscode-button-hoverBackground);
}
.rules-list {
margin-top: 16px;
}
.rule-item {
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
padding: 12px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
.rule-item-name {
color: var(--vscode-foreground);
font-size: 13px;
}
.rule-item > div:first-child svg {
background-color: rgba(148, 204, 241, 0.3);
border-radius: 4px;
padding: 4px;
}
.rule-item-menu {
position: relative;
}
.rule-menu-icon {
width: 20px;
height: 20px;
cursor: pointer;
padding: 4px;
border-radius: 3px;
}
.rule-menu-icon:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.rule-dropdown {
position: absolute;
right: 0;
top: 28px;
background: var(--vscode-menu-background);
border: 1px solid var(--vscode-menu-border);
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
z-index: 100;
min-width: 100px;
}
.rule-dropdown button {
display: block;
width: 100%;
padding: 8px 12px;
background: transparent;
color: var(--vscode-menu-foreground);
border: none;
text-align: left;
cursor: pointer;
font-size: 13px;
}
.rule-dropdown button:hover {
background: var(--vscode-menu-selectionBackground);
color: var(--vscode-menu-selectionForeground);
}
.rule-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.rule-modal-content {
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-input-border);
border-radius: 6px;
padding: 20px;
width: 500px;
max-width: 90%;
}
.rule-modal-content h4 {
margin: 0 0 16px 0;
color: var(--vscode-foreground);
}
.rule-name-input {
width: 100%;
padding: 8px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
font-size: 13px;
margin-bottom: 12px;
box-sizing: border-box;
}
.rule-name-input:focus {
outline: none;
border-color: var(--vscode-focusBorder);
}
.rule-textarea {
width: 100%;
padding: 12px;
background: var(--vscode-input-background);
@ -98,26 +221,20 @@ export function getRulesSettingsComponentStyles(): string {
border-radius: 4px;
font-size: 13px;
font-family: var(--vscode-editor-font-family);
line-height: 1.5;
resize: vertical;
outline: none;
box-sizing: border-box;
}
.rules-textarea:focus {
.rule-textarea:focus {
border-color: var(--vscode-focusBorder);
}
.rules-textarea::placeholder {
color: var(--vscode-input-placeholderForeground);
opacity: 0.6;
}
.rules-textarea-hint {
margin-top: 8px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
font-style: italic;
.rule-modal-actions {
display: flex;
gap: 8px;
margin-top: 16px;
justify-content: flex-end;
}
`;
}
@ -127,51 +244,163 @@ export function getRulesSettingsComponentStyles(): string {
*/
export function getRulesSettingsComponentScript(): string {
return `
// 保存规则设置
function saveRulesSettings() {
const settings = {
enableCustomRules: document.getElementById('enableCustomRulesCheckbox').checked,
systemRules: document.getElementById('systemRulesTextarea').value,
codeRules: document.getElementById('codeRulesTextarea').value,
verilogRules: document.getElementById('verilogRulesTextarea').value,
};
let currentRules = [];
let editingRule = null;
let deletingFilename = null;
// 发送消息到扩展
vscode.postMessage({
command: 'saveRulesSettings',
settings: settings
});
// 显示保存成功提示
console.log('规则设置已保存', settings);
// 显示添加规则弹窗
function showAddRuleModal() {
editingRule = null;
document.getElementById('modalTitle').textContent = '创建个人规则';
document.getElementById('ruleNameInput').value = '';
document.getElementById('ruleTextarea').value = '';
document.getElementById('ruleModal').style.display = 'flex';
}
// 重置规则设置
function resetRulesSettings() {
document.getElementById('enableCustomRulesCheckbox').checked = true;
document.getElementById('systemRulesTextarea').value = '';
document.getElementById('codeRulesTextarea').value = '';
document.getElementById('verilogRulesTextarea').value = '';
console.log('规则设置已重置为默认值');
// 关闭弹窗
function closeRuleModal() {
document.getElementById('ruleModal').style.display = 'none';
closeAllDropdowns();
}
// 加载规则设置
function loadRulesSettings(settings) {
if (!settings) return;
if (settings.enableCustomRules !== undefined) {
document.getElementById('enableCustomRulesCheckbox').checked = settings.enableCustomRules;
}
if (settings.systemRules) {
document.getElementById('systemRulesTextarea').value = settings.systemRules;
}
if (settings.codeRules) {
document.getElementById('codeRulesTextarea').value = settings.codeRules;
}
if (settings.verilogRules) {
document.getElementById('verilogRulesTextarea').value = settings.verilogRules;
// 切换下拉菜单
function toggleDropdown(filename, event) {
event.stopPropagation();
closeAllDropdowns();
const dropdown = document.getElementById('dropdown-' + filename);
if (dropdown) {
dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
}
}
// 关闭所有下拉菜单
function closeAllDropdowns() {
document.querySelectorAll('.rule-dropdown').forEach(d => d.style.display = 'none');
}
// 点击页面其他地方关闭下拉菜单
document.addEventListener('click', closeAllDropdowns);
// 编辑规则
function editRule(filename) {
const rule = currentRules.find(r => r.filename === filename);
if (rule) {
editingRule = rule;
document.getElementById('modalTitle').textContent = '修改个人规则';
document.getElementById('ruleNameInput').value = rule.name;
document.getElementById('ruleTextarea').value = rule.content;
document.getElementById('ruleModal').style.display = 'flex';
}
}
// 保存规则
function saveRule() {
const name = document.getElementById('ruleNameInput').value.trim();
const content = document.getElementById('ruleTextarea').value.trim();
if (!name) {
alert('规则名称不能为空');
return;
}
if (!content) {
alert('规则内容不能为空');
return;
}
const enabled = document.getElementById('enablePersonalRulesCheckbox').checked;
if (editingRule) {
vscode.postMessage({
command: 'updatePersonalRule',
filename: editingRule.filename,
name: name,
content: content,
enabled: enabled
});
} else {
vscode.postMessage({
command: 'savePersonalRule',
name: name,
content: content,
enabled: enabled
});
}
closeRuleModal();
}
// 删除规则
function deleteRule(filename) {
closeAllDropdowns();
const rule = currentRules.find(r => r.filename === filename);
const ruleName = rule ? rule.name : filename;
deletingFilename = filename;
document.getElementById('deleteConfirmText').textContent = '确定要删除规则"' + ruleName + '"吗?此操作无法撤销。';
document.getElementById('deleteConfirmModal').style.display = 'flex';
}
// 关闭删除确认弹窗
function closeDeleteConfirmModal() {
document.getElementById('deleteConfirmModal').style.display = 'none';
deletingFilename = null;
}
// 确认删除
function confirmDelete() {
if (deletingFilename) {
vscode.postMessage({
command: 'deletePersonalRule',
filename: deletingFilename
});
}
closeDeleteConfirmModal();
}
// 渲染规则列表
function renderRulesList(rules) {
currentRules = rules || [];
const listEl = document.getElementById('rulesList');
if (currentRules.length === 0) {
listEl.innerHTML = '<div style="color: var(--vscode-descriptionForeground); padding: 16px; text-align: center;">暂无规则,点击"+ 创建"添加</div>';
return;
}
const peopleRulesIcon = '${peopleRules}';
listEl.innerHTML = currentRules.map(rule => \`
<div class="rule-item">
<div style="display: flex; align-items: center; gap: 8px;">
\${peopleRulesIcon}
<div class="rule-item-name">\${rule.filename}</div>
</div>
<div class="rule-item-menu">
<svg class="rule-menu-icon" onclick="toggleDropdown('\${rule.filename}', event)" viewBox="0 0 16 16" fill="currentColor">
<circle cx="8" cy="3" r="1.5"/>
<circle cx="8" cy="8" r="1.5"/>
<circle cx="8" cy="13" r="1.5"/>
</svg>
<div class="rule-dropdown" id="dropdown-\${rule.filename}" style="display: none;">
<button onclick="editRule('\${rule.filename}')">编辑</button>
<button onclick="deleteRule('\${rule.filename}')">删除</button>
</div>
</div>
</div>
\`).join('');
}
// 加载规则列表
function loadPersonalRules(data) {
if (data && data.enabled !== undefined) {
document.getElementById('enablePersonalRulesCheckbox').checked = data.enabled;
}
if (data && data.rules) {
renderRulesList(data.rules);
}
}
// 页面加载时请求规则数据
vscode.postMessage({ command: 'loadPersonalRules' });
`;
}

View File

@ -715,6 +715,13 @@ export function getWebviewContent(
}
break;
case 'personalRulesLoaded':
// 加载个人规则数据
if (typeof loadPersonalRules === 'function') {
loadPersonalRules(message.data);
}
break;
case 'autoSendMessage':
// 自动发送待发送的消息(登录后)
console.log('[WebView] 自动发送待发送消息:', message.text);

View File

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