31 Commits

Author SHA1 Message Date
251289a340 Merge branch 'feat/plugin-front-end' into merge/250105merge 2026-01-05 19:08:27 +08:00
c22081c5e9 feat: 处理后端 heartbeat 事件,保持 SSE 连接活跃 2026-01-05 19:04:04 +08:00
cca82c7885 feat:将todo的需要改为勾选框
- 为后续的todo完成打勾做准备
2026-01-05 18:30:59 +08:00
e4ff49bade chore: 添加 vcdParser.ts (未使用,保留备用)
纯 TypeScript 实现的 VCD 解析器,当前未使用。
目前使用 waveformTracer.ts 调用 Python 打包的 exe。
2026-01-05 18:29:49 +08:00
ada4806493 feat: 集成 waveform_trace 波形调试工具
新增功能:
- waveformTracer.ts: 调用 waveform_trace.exe 的工具实现
- toolExecutor.ts: 添加 waveform_trace 工具分发
- types/api.ts: 添加 WaveformTraceArgs 类型定义

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

配置文件:
- .gitignore: 排除 exe 和打包产物
- .vscodeignore: 发布时排除源码
- build.bat/build.sh: 打包脚本
2026-01-05 18:18:57 +08:00
3831de2849 fix: 修复 ICViewProvider 中的事件监听器内存泄漏问题
将 webview.onDidReceiveMessage 监听器添加到 context.subscriptions 中,
   确保在扩展停用时能够正确清理,避免潜在的内存泄漏。
2026-01-05 16:40:32 +08:00
0df529c4fd feat:实现思考的组件 2026-01-05 16:25:47 +08:00
5c53d7f0e9 feat:修改模式内容和增加icon 2026-01-05 16:22:52 +08:00
ef2a0dc16e feat:修改模型描述的展现形式和内容 2026-01-05 16:19:53 +08:00
5ce420295b feat:解决图片没有被打包进去的bug 2026-01-05 16:12:15 +08:00
1d7f3d7626 feat:添加上下文功能实现 2026-01-05 15:59:26 +08:00
9b0d2d5e01 feat:进度条收起的功能和发起对话才展示 2026-01-05 15:27:40 +08:00
27e3351b55 feat:输入框居中展示
- 点击历史记录和发起对话之后回到底部
2026-01-05 15:18:03 +08:00
de3e84aa4e feat:顶部添加进度条 2026-01-05 11:27:06 +08:00
e48e822d07 fix: 修复 taskId 不一致导致 conversation.json 找不到的问题
- messageHandler 复用 historyManager 的 taskId 而非重新生成
- 环境切换为 dev,超时时间统一为 5 分钟
- agentCard 添加调试智能体相关工具名称映射
- 移除冗余的 segments 调试日志
2026-01-05 10:15:25 +08:00
8dc34ee435 feat:让用户看不懂的工具隐晦展示 2026-01-04 16:29:13 +08:00
d8cd86361e feat: 添加获取当前环境的功能以控制快速操作按钮的显示 2026-01-04 14:10:43 +08:00
acf3f9ff37 feat: 添加模型图标支持并更新相关组件以显示图标 2026-01-04 10:56:57 +08:00
c27b08cccf feat: 将当前环境修改为测试环境并调整模型选择器的选项顺序 2026-01-04 10:39:15 +08:00
9fc3c9f056 feat: 将当前环境从测试环境切换为生产环境 2025-12-31 19:10:00 +08:00
60d8eaf0eb feat: 修改当前环境为测试环境并调整后端服务地址注释格式 2025-12-31 19:08:37 +08:00
df6f983e83 Merge branch 'feat/back-to-front' into feat/plugin-front-end 2025-12-31 19:00:23 +08:00
acf60f2a17 feat: 添加消息区域操作按钮,包括复制、点赞和点踩功能 2025-12-31 18:51:17 +08:00
f933d84cd1 feat: 新增会话压缩命令和上下文显示功能
- ICHelperPanel: 新增 compressConversation 命令处理,支持手动触发会话压缩
- ICHelperPanel: 在加载历史会话时设置 lastTaskId,确保压缩操作可用
- webviewContent: 新增 contextUsage 消息处理,更新上下文使用量显示
- userInteraction: 将用户回答超时时间从 5 分钟延长至 2 小时
2025-12-31 18:50:27 +08:00
b794d1ceb0 feat: 实现上下文使用量监控和会话压缩功能
- sseHandler: 新增 onContextUsage 回调处理上下文使用量事件
- dialogService: 集成上下文使用量回调,追踪 AI 消息用于后端重启恢复
- apiClient: 新增 compactDialog API 支持手动压缩对话历史
- messageHandler: 新增 lastTaskId 管理机制,支持会话恢复后的压缩操作,转发上下文使用量到 WebView
2025-12-31 18:50:20 +08:00
259310a29d feat: 新增上下文使用量事件类型定义
- 新增 context_usage 事件类型
- 新增 ContextUsageEvent 接口,包含当前 token 数、最大 token 数和使用百分比
- 用于实时监控对话上下文的使用情况
2025-12-31 18:50:11 +08:00
715eac5949 feat: 新增多环境配置支持
- 新增 dev/test/prod 三种环境配置
- 支持通过 CURRENT_ENV 常量快速切换环境
- 重构配置获取逻辑,使用环境映射表
- 新增 getCurrentEnv() 方法获取当前环境
2025-12-31 18:50:05 +08:00
c2936395d9 refactor: 优化代码结构,简化导入语句并注释掉快速操作部分 2025-12-31 18:16:04 +08:00
8762eacb3e feat: 增强输入框状态管理,添加禁用状态和恢复输入状态的逻辑 2025-12-31 18:13:21 +08:00
3d535fd3e1 fix: 优化后端服务不可用时的错误处理,移除本地模拟回复逻辑 2025-12-31 18:02:38 +08:00
ecdbe0bdc0 feat: 更新输入框占位符提示,增加使用说明
- 按 Enter 发送,Shift + Enter 换行
2025-12-31 16:42:23 +08:00
199 changed files with 59825 additions and 387 deletions

11
.gitignore vendored
View File

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

29
.vscodeignore Normal file
View File

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

View File

@ -101,7 +101,8 @@
"files": [
"dist",
"media",
"tools"
"tools",
"src/assets"
],
"dependencies": {
"@wavedrom/doppler": "^1.14.0",

BIN
src/assets/model/Auto.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/assets/model/Max.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
src/assets/model/Sy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/assets/model/lite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@ -1,9 +1,15 @@
/**
* 配置管理
* 后端地址已预配置,用户无需手动设置
* 支持 dev本地开发和 test测试服务器两种环境
*/
import * as vscode from "vscode";
/** 环境类型 */
type Environment = "dev" | "test" | "prod";
/** 当前环境 - 修改这里切换环境 */
const CURRENT_ENV: Environment = "dev";
/** 配置项接口 */
export interface IccoderConfig {
/** 后端服务地址 */
@ -14,19 +20,40 @@ export interface IccoderConfig {
userId: string;
}
/** 默认配置(预配置,不暴露给用户) */
const DEFAULT_CONFIG: IccoderConfig = {
backendUrl: "http://192.168.1.108:2233",
timeout: 60000,
userId: "default-user",
/** 环境配置 */
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
/** 本地开发环境 */
dev: {
backendUrl: "http://localhost:2233",
timeout: 300000, // 5分钟与子智能体超时一致
userId: "default-user",
},
/** 测试服务器环境 */
test: {
backendUrl: "http://192.168.1.108:2233",
timeout: 60000,
userId: "default-user",
},
/** 生产环境 */
prod: {
backendUrl: "https://api.iccoder.com", // TODO: 替换为实际生产地址
timeout: 60000,
userId: "default-user",
},
};
/**
* 获取当前环境
*/
export function getCurrentEnv(): Environment {
return CURRENT_ENV;
}
/**
* 获取配置项
* 直接返回预配置的值,用户无需手动配置
*/
export function getConfig(): IccoderConfig {
return { ...DEFAULT_CONFIG };
return { ...ENV_CONFIG[CURRENT_ENV] };
}
/**
@ -34,7 +61,6 @@ export function getConfig(): IccoderConfig {
*/
export function getApiUrl(path: string): string {
const { backendUrl } = getConfig();
// 确保 URL 格式正确
const baseUrl = backendUrl.endsWith("/")
? backendUrl.slice(0, -1)
: backendUrl;

View File

@ -82,6 +82,11 @@ export const agentIconSvg = `
*/
export const plannerIconSvg = `<svg t="1767143425474" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10954" width="16" height="16"><path d="M860.544 633.856c-82.368 0-152.128 69.632-158.464 152h-354.88c-31.616 0-63.296-31.68-63.296-63.296V437.376c12.608 0 25.344 6.4 44.288 6.4h380.16c12.672 69.696 76.032 126.656 152.128 126.656 88.704 0 158.336-69.696 158.336-158.4s-69.632-158.4-158.336-158.4c-76.096 0-139.456 57.024-152.128 126.656h-361.216c-31.616 0-63.296-31.68-63.296-63.296v-133.12h164.736c31.68 0 63.296-22.848 63.296-54.528a55.04 55.04 0 0 0-56-56h-380.16c-31.68 0-70.72 17.984-70.72 56s31.68 54.528 63.36 54.528h133.056v538.624c0 69.696 57.088 126.656 126.72 126.656h386.56c25.344 57.088 82.368 101.376 145.728 101.376a156.8 156.8 0 0 0 158.336-158.4 156.608 156.608 0 0 0-158.208-158.272z m0-316.8c50.624 0 94.912 44.288 94.912 94.976s-44.288 94.976-94.912 94.976c-50.752 0-95.104-44.288-95.104-94.976s44.352-94.976 95.104-94.976z m0 570.24c-50.752 0-95.104-44.352-95.104-95.04s44.352-95.04 95.104-95.04c50.624 0 94.912 44.352 94.912 95.04s-44.288 95.04-94.912 95.04z" p-id="10955" fill="#8a8a8a"></path></svg>`;
/**
* Ask 模式图标 SVG
*/
export const askIconSvg = `<svg t="1767143500000" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="#8a8a8a"/><path d="M623.6 316.7C593.6 290.4 554 276 512 276s-81.6 14.5-111.6 40.7C369.2 344 352 380.7 352 420.4c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8 0-25.6 10.1-49.4 28.4-67.2 18.7-18.2 43.4-28.2 71.6-28.2s52.9 10 71.6 28.2c18.3 17.8 28.4 41.6 28.4 67.2 0 29.5-12.2 55.3-36.2 76.6-23.2 20.6-61.1 45.9-82.2 60.6-17.8 12.4-28.6 32.7-28.6 54.2V640c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-35.8c0-4.1 2.6-7.8 6.5-9.2 31.3-11.6 84.8-40.6 113.8-64.8 42.6-35.6 66.2-83.5 66.2-134.8 0-39.7-17.2-76.4-48.4-103.3z" fill="#8a8a8a"/><path d="M512 716m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#8a8a8a"/></svg>`;
/**
* 保存知识库图标 SVG
*/

View File

@ -12,7 +12,9 @@ import {
handlePlanAction,
setPendingPlanExecution,
getCurrentTaskId,
setLastTaskId,
} from "../utils/messageHandler";
import { compactDialog } from "../services/apiClient";
import { VCDViewerPanel } from "./VCDViewerPanel";
import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { MessageType } from "../types/chatHistory";
@ -58,7 +60,10 @@ export async function showICHelperPanel(
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
localResourceRoots: [
vscode.Uri.joinPath(context.extensionUri, "media"),
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
],
}
);
@ -80,8 +85,28 @@ export async function showICHelperPanel(
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
);
// 获取模型图标URI
const autoIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
);
const liteIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
);
const syIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
);
const maxIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
);
// 设置HTML内容
panel.webview.html = getWebviewContent(iconUri.toString());
panel.webview.html = getWebviewContent(
iconUri.toString(),
autoIconUri.toString(),
liteIconUri.toString(),
syIconUri.toString(),
maxIconUri.toString()
);
// 处理消息
panel.webview.onDidReceiveMessage(
@ -116,6 +141,9 @@ export async function showICHelperPanel(
// 切换到当前面板的任务上下文
historyManager.switchToPanelTask(panelId);
// 显示进度条
panel.webview.postMessage({ type: 'showProgress' });
handleUserMessage(
panel,
message.text,
@ -195,6 +223,39 @@ export async function showICHelperPanel(
case "abortDialog":
void abortCurrentDialog();
break;
// 新增:压缩会话
case "compressConversation":
{
const taskId = getCurrentTaskId();
if (taskId) {
compactDialog(taskId)
.then((result) => {
if (result.success) {
panel.webview.postMessage({
command: "receiveMessage",
text: "✅ 会话压缩完成",
});
} else {
panel.webview.postMessage({
command: "receiveMessage",
text: `❌ 压缩失败: ${result.error || "未知错误"}`,
});
}
})
.catch((err) => {
panel.webview.postMessage({
command: "receiveMessage",
text: `❌ 压缩失败: ${err.message || "网络错误"}`,
});
});
} else {
panel.webview.postMessage({
command: "receiveMessage",
text: "❌ 没有活跃的会话",
});
}
}
break;
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
case "planAction":
if (message.action === "confirm") {
@ -220,6 +281,109 @@ export async function showICHelperPanel(
}
}
break;
// 添加文件上下文 - 显示工作区文件列表
case "addContextFile":
{
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
vscode.window.showWarningMessage("请先打开一个工作区");
break;
}
// 获取工作区所有文件
const files = await vscode.workspace.findFiles(
"**/*",
"**/node_modules/**"
);
panel.webview.postMessage({
command: "showWorkspaceFileList",
files: files.map((uri) => ({
path: uri.fsPath,
relativePath: vscode.workspace.asRelativePath(uri),
})),
});
}
break;
// 添加文件夹上下文 - 显示工作区文件夹列表
case "addContextFolder":
{
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
vscode.window.showWarningMessage("请先打开一个工作区");
break;
}
// 获取工作区所有文件夹
const fs = require("fs");
const path = require("path");
const folders: Array<{ path: string; relativePath: string }> = [];
function scanFolders(dir: string, baseDir: string) {
try {
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
if (item.isDirectory() && item.name !== "node_modules" && !item.name.startsWith(".")) {
const fullPath = path.join(dir, item.name);
const relativePath = path.relative(baseDir, fullPath);
folders.push({ path: fullPath, relativePath });
scanFolders(fullPath, baseDir);
}
}
} catch (error) {
console.error("扫描文件夹失败:", error);
}
}
scanFolders(workspaceFolder.uri.fsPath, workspaceFolder.uri.fsPath);
panel.webview.postMessage({
command: "showWorkspaceFolderList",
folders: folders,
});
}
break;
// 添加图片上下文
case "addContextImage":
{
const imageUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: true,
openLabel: "选择图片",
filters: {
"图片文件": ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
},
});
if (imageUris && imageUris.length > 0) {
panel.webview.postMessage({
command: "contextImagesSelected",
images: imageUris.map((uri) => uri.fsPath),
});
}
}
break;
// 添加文档库上下文
case "addContextDocument":
{
const docUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: true,
openLabel: "选择文档",
filters: {
"文档文件": ["pdf", "doc", "docx", "txt", "md"],
"所有文件": ["*"],
},
});
if (docUris && docUris.length > 0) {
panel.webview.postMessage({
command: "contextDocumentsSelected",
documents: docUris.map((uri) => uri.fsPath),
});
}
}
break;
// 新增:检查工作区状态
case "checkWorkspace":
const hasWorkspace = !!(
@ -528,6 +692,9 @@ async function selectConversation(
return;
}
// 设置 lastTaskId用于压缩等操作
setLastTaskId(taskId);
// 更新面板的任务映射,确保后续对话保存到正确的任务中
const panelId = (panel as any).__uniqueId;
historyManager.setPanelTask(panelId, taskId, workspacePath);

View File

@ -155,6 +155,26 @@ export async function stopDialog(taskId: string): Promise<StopDialogResponse> {
});
}
/** 压缩对话响应 */
export interface CompactDialogResponse {
success: boolean;
taskId: string;
message?: string;
error?: string;
}
/**
* 手动压缩对话历史
* POST /api/dialog/compact
*/
export async function compactDialog(taskId: string): Promise<CompactDialogResponse> {
console.log(`[API] 压缩对话: taskId=${taskId}`);
return request<CompactDialogResponse>('/api/dialog/compact', {
method: 'POST',
body: { taskId }
});
}
/**
* 创建成功的工具结果
*/

View File

@ -73,6 +73,8 @@ export interface DialogCallbacks {
onError?: (message: string) => void;
/** 通知消息 */
onNotification?: (message: string) => void;
/** 上下文使用量更新 */
onContextUsage?: (data: { currentTokens: number; maxTokens: number; percentage: number }) => void;
}
/**
@ -553,6 +555,12 @@ export class DialogSession {
onComplete: (data) => {
this.isActive = false;
this.finalizeTextSegment();
// 追踪 AI 消息(用于后端重启后恢复)
if (this.accumulatedText) {
historyManager.trackAiMessage(this.accumulatedText);
}
// 发送所有段落
callbacks.onComplete?.(this.segments);
},
@ -639,6 +647,11 @@ export class DialogSession {
await historyManager.saveCompactedData(data.compactedData);
},
onContextUsage: (data) => {
console.log('[DialogSession] onContextUsage:', data.currentTokens, '/', data.maxTokens);
callbacks.onContextUsage?.(data);
},
onOpen: () => {
console.log('[DialogSession] SSE 连接已建立');
},

View File

@ -27,7 +27,8 @@ import type {
AgentStartEvent,
AgentProgressEvent,
AgentCompleteEvent,
AgentErrorEvent
AgentErrorEvent,
ContextUsageEvent
} from '../types/api';
import type { MemoryCompactedEvent } from '../types/memory';
@ -71,6 +72,8 @@ export interface SSECallbacks {
onAgentError?: (data: AgentErrorEvent) => void;
/** 记忆压缩完成 */
onMemoryCompacted?: (data: MemoryCompactedEvent) => void;
/** 上下文使用量更新 */
onContextUsage?: (data: ContextUsageEvent) => void;
/** 连接打开 */
onOpen?: () => void;
/** 连接关闭 */
@ -325,6 +328,14 @@ function dispatchEvent(
case 'memory_compacted':
callbacks.onMemoryCompacted?.(data as MemoryCompactedEvent);
break;
case 'context_usage':
callbacks.onContextUsage?.(data as ContextUsageEvent);
break;
case 'heartbeat':
// 心跳事件:仅用于保持连接,不需要特殊处理
// Node.js req.setTimeout 会在收到数据时自动重置计时器
console.log('[SSE] 收到心跳');
break;
default:
console.log(`[SSE] 未知事件类型: ${eventType}`, data);
}

View File

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

View File

@ -67,13 +67,13 @@ export class UserInteractionManager {
reject
});
// 设置超时(5分钟
// 设置超时(2小时
setTimeout(() => {
if (this.pendingQuestions.has(askId)) {
this.pendingQuestions.delete(askId);
reject(new Error('用户回答超时'));
}
}, 300000);
}, 7200000);
});
}

View File

@ -54,6 +54,7 @@ export type SSEEventType =
| 'agent_complete' // 子智能体完成
| 'agent_error' // 子智能体错误
| 'memory_compacted' // 记忆压缩完成
| 'context_usage' // 上下文使用量
| 'complete' // 对话完成
| 'error' // 错误
| 'warning' // 警告
@ -181,6 +182,13 @@ export interface AgentErrorEvent {
timestamp: number;
}
/** context_usage 事件数据 */
export interface ContextUsageEvent {
currentTokens: number;
maxTokens: number;
percentage: number;
}
// ============== 工具调用协议 (MCP 格式) ==============
/**
@ -301,6 +309,7 @@ export type ToolName =
| 'syntax_check'
| 'simulation'
| 'waveform_summary'
| 'waveform_trace'
| 'knowledge_save'
| 'knowledge_load';
@ -346,6 +355,18 @@ export interface WaveformSummaryArgs {
checkpoints?: string;
}
/** waveform_trace 工具参数 */
export interface WaveformTraceArgs {
/** Verilog 源文件路径(相对于项目根目录) */
verilogPath: string;
/** VCD 波形文件路径(相对于项目根目录) */
vcdPath: string;
/** 仿真工具的输出字符串(包含 mismatch 信息) */
simOutput: string;
/** BFS 回溯层数,默认 2 */
traceLevel?: number;
}
/** knowledge_save 工具参数 */
export interface KnowledgeSaveArgs {
/** 知识图谱 JSON 数据 */
@ -366,5 +387,6 @@ export type ToolArgs =
| SyntaxCheckArgs
| SimulationArgs
| WaveformSummaryArgs
| WaveformTraceArgs
| KnowledgeSaveArgs
| KnowledgeLoadArgs;

View File

@ -19,7 +19,7 @@ import { dialogManager, DialogSession } from "../services/dialogService";
import { userInteractionManager } from "../services/userInteraction";
import { healthCheck } from "../services/apiClient";
import type { RunMode } from '../types/api';
import type { RunMode } from "../types/api";
/** 是否使用后端服务(可通过配置控制) */
let useBackendService = true;
@ -27,12 +27,15 @@ let useBackendService = true;
/** 当前对话会话 */
let currentSession: DialogSession | null = null;
/** 最后一个活跃的 taskId用于压缩等操作 */
let lastTaskId: string | null = null;
/** 待执行的计划Plan 模式确认后自动执行) */
let pendingPlanExecution: {
panel: vscode.WebviewPanel;
planTitle: string;
extensionPath: string;
taskId: string; // 保存 taskId 以便复用
taskId: string; // 保存 taskId 以便复用
} | null = null;
/**
@ -45,7 +48,7 @@ export function setPendingPlanExecution(
taskId: string
): void {
pendingPlanExecution = { panel, planTitle, extensionPath, taskId };
console.log('[MessageHandler] 设置待执行计划:', planTitle, 'taskId:', taskId);
console.log("[MessageHandler] 设置待执行计划:", planTitle, "taskId:", taskId);
}
/**
@ -90,29 +93,28 @@ export async function handleUserMessage(
await handleUserMessageWithBackend(panel, text, extensionPath, mode);
return;
} catch (error) {
console.error("后端服务不可用,回退到本地模式:", error);
// 后端不可用时,使用本地模拟回复
console.error("后端服务不可用:", error);
panel.webview.postMessage({
command: "updateStatus",
text: "后端服务不可用",
type: "error",
});
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
throw error;
}
}
// 本地模拟回复(后端不可用时的 fallback
console.log("使用本地模拟回复");
const reply = getMockReply(text);
// 记录AI回复到历史允许失败
try {
const historyManager = ChatHistoryManager.getInstance();
await historyManager.addAiMessage(reply);
} catch (error) {
console.warn("记录AI回复历史失败:", error);
}
setTimeout(() => {
panel.webview.postMessage({
command: "receiveMessage",
text: reply,
});
}, 500);
// 如果没有 extensionPath显示错误
panel.webview.postMessage({
command: "updateStatus",
text: "无法处理消息:缺少必要参数",
type: "error",
});
}
/**
@ -123,18 +125,22 @@ async function handleUserMessageWithBackend(
text: string,
extensionPath: string,
mode?: RunMode,
reuseTaskId?: string // 可选,复用现有 taskId用于 Plan 模式确认后继续执行)
reuseTaskId?: string // 可选,复用现有 taskId用于 Plan 模式确认后继续执行)
): Promise<void> {
const historyManager = ChatHistoryManager.getInstance();
// 获取 historyManager 中的 taskId由 ICHelperPanel 创建)
// 优先使用 reuseTaskId其次使用 historyManager 的 taskId
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
// 创建或复用会话
if (!currentSession || !currentSession.active) {
currentSession = dialogManager.createSession(extensionPath, reuseTaskId);
if (reuseTaskId) {
console.log('[MessageHandler] 复用 taskId 创建会话:', reuseTaskId);
}
currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined);
// 保存 taskId 用于后续操作(如压缩)
lastTaskId = currentSession.getTaskId();
console.log("[MessageHandler] 创建会话: taskId=", lastTaskId, "来源=", taskIdToUse ? "historyManager" : "新生成");
}
const historyManager = ChatHistoryManager.getInstance();
// 显示状态栏
panel.webview.postMessage({
command: "updateStatus",
@ -143,117 +149,146 @@ async function handleUserMessageWithBackend(
});
return new Promise((resolve, reject) => {
currentSession!.sendMessage(text, {
onText: (fullText, isStreaming) => {
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
currentSession!.sendMessage(
text,
{
onText: (fullText, isStreaming) => {
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
},
onSegmentUpdate: (segments) => {
// 实时发送段落更新,按后端返回顺序展示
panel.webview.postMessage({
command: "updateSegments",
segments: segments,
});
},
onToolStart: (toolName) => {
// 更新状态栏
panel.webview.postMessage({
command: "updateStatus",
text: `正在执行 ${toolName}...`,
type: "working",
});
},
onToolComplete: (toolName, result) => {
// 工具完成,不需要单独处理,通过 onSegmentUpdate 统一更新
},
onToolError: (toolName, error) => {
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
},
onQuestion: (askId, question, options) => {
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
panel.webview.postMessage({
command: "updateStatus",
text: "等待用户回答...",
type: "working",
});
},
onComplete: async (segments) => {
// 隐藏状态栏
panel.webview.postMessage({
command: "hideStatus",
});
// 最后一次发送完整的段落
console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
const result = await panel.webview.postMessage({
command: "updateSegments",
segments: segments,
isComplete: true,
});
console.log("[MessageHandler] postMessage 返回值:", result);
// 保存完整的 segments 到历史记录
try {
// 将完整的 segments 保存到一条 AI 消息中
// 这样加载时可以完整还原对话样式
const textContent = segments
.filter((s) => s.type === "text" && s.content)
.map((s) => s.content)
.join("\n");
await historyManager.addAiMessage(textContent, undefined, segments);
} catch (error) {
console.warn("保存AI响应历史失败:", error);
}
// 检查是否有待执行的计划Plan 模式确认后自动执行)
if (pendingPlanExecution) {
const {
panel: execPanel,
planTitle,
extensionPath: execPath,
taskId: reuseTaskId,
} = pendingPlanExecution;
pendingPlanExecution = null;
console.log(
"[MessageHandler] 自动执行计划:",
planTitle,
"复用 taskId:",
reuseTaskId
);
// 延迟一小段时间确保当前对话完全结束
setTimeout(async () => {
try {
// 复用 taskId 创建新会话,确保知识图谱数据不丢失
await handleUserMessageWithBackend(
execPanel,
`请按照刚才的计划执行:${planTitle}`,
execPath,
"agent",
reuseTaskId // 复用 Plan 模式的 taskId
);
} catch (err) {
console.error("[MessageHandler] 自动执行计划失败:", err);
}
}, 500);
}
resolve();
},
onError: (message) => {
panel.webview.postMessage({
command: "hideLoading",
});
panel.webview.postMessage({
command: "receiveMessage",
text: `❌ 错误: ${message}`,
});
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
reject(new Error(message));
},
onNotification: (message) => {
vscode.window.showInformationMessage(message);
},
onContextUsage: (data) => {
// 发送上下文使用量到 WebView
panel.webview.postMessage({
command: "contextUsage",
currentTokens: data.currentTokens,
maxTokens: data.maxTokens,
percentage: data.percentage,
});
},
},
onSegmentUpdate: (segments) => {
// 实时发送段落更新,按后端返回顺序展示
panel.webview.postMessage({
command: "updateSegments",
segments: segments,
});
},
onToolStart: (toolName) => {
// 更新状态栏
panel.webview.postMessage({
command: "updateStatus",
text: `正在执行 ${toolName}...`,
type: "working",
});
},
onToolComplete: (toolName, result) => {
// 工具完成,不需要单独处理,通过 onSegmentUpdate 统一更新
},
onToolError: (toolName, error) => {
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
},
onQuestion: (askId, question, options) => {
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
panel.webview.postMessage({
command: "updateStatus",
text: "等待用户回答...",
type: "working",
});
},
onComplete: async (segments) => {
// 隐藏状态栏
panel.webview.postMessage({
command: "hideStatus",
});
// 最后一次发送完整的段落
console.log('[MessageHandler] 对话完成, 段落数:', segments.length);
console.log('[MessageHandler] segments 内容:', JSON.stringify(segments));
const result = await panel.webview.postMessage({
command: "updateSegments",
segments: segments,
isComplete: true,
});
console.log('[MessageHandler] postMessage 返回值:', result);
// 保存完整的 segments 到历史记录
try {
// 将完整的 segments 保存到一条 AI 消息中
// 这样加载时可以完整还原对话样式
const textContent = segments
.filter(s => s.type === 'text' && s.content)
.map(s => s.content)
.join('\n');
await historyManager.addAiMessage(textContent, undefined, segments);
} catch (error) {
console.warn("保存AI响应历史失败:", error);
}
// 检查是否有待执行的计划Plan 模式确认后自动执行)
if (pendingPlanExecution) {
const { panel: execPanel, planTitle, extensionPath: execPath, taskId: reuseTaskId } = pendingPlanExecution;
pendingPlanExecution = null;
console.log('[MessageHandler] 自动执行计划:', planTitle, '复用 taskId:', reuseTaskId);
// 延迟一小段时间确保当前对话完全结束
setTimeout(async () => {
try {
// 复用 taskId 创建新会话,确保知识图谱数据不丢失
await handleUserMessageWithBackend(
execPanel,
`请按照刚才的计划执行:${planTitle}`,
execPath,
'agent',
reuseTaskId // 复用 Plan 模式的 taskId
);
} catch (err) {
console.error('[MessageHandler] 自动执行计划失败:', err);
}
}, 500);
}
resolve();
},
onError: (message) => {
panel.webview.postMessage({
command: "hideLoading",
});
panel.webview.postMessage({
command: "receiveMessage",
text: `❌ 错误: ${message}`,
});
reject(new Error(message));
},
onNotification: (message) => {
vscode.window.showInformationMessage(message);
},
}, mode);
mode
);
});
}
@ -281,16 +316,16 @@ export async function abortCurrentDialog(): Promise<void> {
try {
const historyManager = ChatHistoryManager.getInstance();
const textContent = segments
.filter(s => s.type === 'text' && s.content)
.map(s => s.content)
.join('\n');
.filter((s) => s.type === "text" && s.content)
.map((s) => s.content)
.join("\n");
// 添加中止标记
const abortedContent = textContent + '\n\n[对话已被用户中止]';
const abortedContent = textContent + "\n\n[对话已被用户中止]";
await historyManager.addAiMessage(abortedContent, undefined, segments);
console.log('[MessageHandler] 已保存中止前的对话内容');
console.log("[MessageHandler] 已保存中止前的对话内容");
} catch (error) {
console.warn('[MessageHandler] 保存中止对话失败:', error);
console.warn("[MessageHandler] 保存中止对话失败:", error);
}
}
}
@ -298,8 +333,8 @@ export async function abortCurrentDialog(): Promise<void> {
// 通知 WebView 重置分段消息容器
const panel = userInteractionManager.getWebviewPanel();
if (panel) {
panel.webview.postMessage({ command: 'resetSegmentedMessage' });
console.log('[MessageHandler] 已发送重置分段消息命令');
panel.webview.postMessage({ command: "resetSegmentedMessage" });
console.log("[MessageHandler] 已发送重置分段消息命令");
}
dialogManager.abortCurrentSession();
@ -310,7 +345,15 @@ export async function abortCurrentDialog(): Promise<void> {
* 获取当前会话的 taskId
*/
export function getCurrentTaskId(): string | null {
return currentSession?.getTaskId() || null;
return currentSession?.getTaskId() || lastTaskId;
}
/**
* 设置最后的 taskId加载历史会话时调用
*/
export function setLastTaskId(taskId: string): void {
lastTaskId = taskId;
console.log("[MessageHandler] 设置 lastTaskId:", taskId);
}
/**
@ -326,52 +369,52 @@ export async function handlePlanAction(
planTitle: string,
extensionPath: string
): Promise<void> {
console.log('[handlePlanAction] action:', action, 'planTitle:', planTitle);
console.log("[handlePlanAction] action:", action, "planTitle:", planTitle);
switch (action) {
case 'confirm':
case "confirm":
// 确认执行:切换到 Agent 模式并发送执行消息
panel.webview.postMessage({
command: 'switchMode',
mode: 'agent'
command: "switchMode",
mode: "agent",
});
// 发送执行消息
await handleUserMessage(
panel,
`请按照刚才的计划执行:${planTitle}`,
extensionPath,
'agent'
"agent"
);
break;
case 'modify':
case "modify":
// 修改计划:提示用户输入修改建议
const modification = await vscode.window.showInputBox({
prompt: '请输入您对计划的修改建议',
placeHolder: '例如第2步需要先检查文件是否存在...',
ignoreFocusOut: true
prompt: "请输入您对计划的修改建议",
placeHolder: "例如第2步需要先检查文件是否存在...",
ignoreFocusOut: true,
});
if (modification) {
await handleUserMessage(
panel,
`请根据以下建议修改计划:${modification}`,
extensionPath,
'plan'
"plan"
);
}
break;
case 'cancel':
case "cancel":
// 取消计划:通知用户
panel.webview.postMessage({
command: 'addMessage',
text: '计划已取消。',
sender: 'bot'
command: "addMessage",
text: "计划已取消。",
sender: "bot",
});
break;
default:
console.warn('[handlePlanAction] 未知操作:', action);
console.warn("[handlePlanAction] 未知操作:", action);
}
}
@ -410,7 +453,9 @@ function parseFileOperation(text: string): {
}
// 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts优先匹配避免被修改匹配
const renameMatch = lowerText.match(/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/);
const renameMatch = lowerText.match(
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/
);
if (renameMatch) {
const oldPath = renameMatch[1].trim();
const newPath = renameMatch[2].trim();
@ -425,7 +470,9 @@ function parseFileOperation(text: string): {
// 格式1: 在 xxx.ts 中将 "aaa" 替换为 "bbb"
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
// 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb'
const replaceMatch1 = lowerText.match(/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/);
const replaceMatch1 = lowerText.match(
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
);
if (replaceMatch1) {
const filePath = replaceMatch1[1].trim();
const searchText = replaceMatch1[2].trim();
@ -439,7 +486,9 @@ function parseFileOperation(text: string): {
}
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
const replaceMatch2 = lowerText.match(/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/);
const replaceMatch2 = lowerText.match(
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
);
if (replaceMatch2) {
const filePath = replaceMatch2[1].trim();
const searchText = replaceMatch2[2].trim();
@ -767,41 +816,6 @@ export async function handleReplaceInFile(
}
}
/**
* 获取模拟回复
*/
function getMockReply(question: string): string {
const replies = [
`已收到您的问题:"${question}"
这是一个演示版本实际需要连接AI服务。
示例回复:这是一个计数器模板:
\`\`\`verilog
module counter (
input clk,
input rst_n,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= 0;
else count <= count + 1;
end
endmodule
\`\`\``,
`感谢提问!关于"${question}",在真实版本中我会:
1. 分析您的代码上下文
2. 提供优化建议
3. 生成完整代码
4. 解释设计原理
当前是演示版,请点击侧边栏按钮快速生成代码。`,
];
return replies[Math.floor(Math.random() * replies.length)];
}
/**
* 将代码插入到编辑器
*/
@ -894,7 +908,8 @@ async function handleVCDGeneration(
if (!projectCheck.hasTestbench) {
errorMsg += "• ❌ 缺少 testbench 文件\n";
errorMsg += "\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
errorMsg +=
"\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
} else {
errorMsg += `• ✅ Testbench: ${projectCheck.testbenchFile}\n`;
}
@ -938,9 +953,7 @@ async function handleVCDGeneration(
fileName: fileName,
});
vscode.window.showInformationMessage(
`VCD 文件生成成功: ${fileName}`
);
vscode.window.showInformationMessage(`VCD 文件生成成功: ${fileName}`);
} else {
panel.webview.postMessage({
command: "receiveMessage",

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

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

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

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

View File

@ -24,7 +24,10 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
localResourceRoots: [
vscode.Uri.joinPath(context.extensionUri, "media"),
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
],
}
);
@ -39,8 +42,29 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
const iconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
);
// 获取模型图标URI
const autoIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
);
const liteIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
);
const syIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
);
const maxIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
);
// 设置HTML内容
panel.webview.html = getWebviewContent(iconUri.toString());
panel.webview.html = getWebviewContent(
iconUri.toString(),
autoIconUri.toString(),
liteIconUri.toString(),
syIconUri.toString(),
maxIconUri.toString()
);
// 处理消息
panel.webview.onDidReceiveMessage(
@ -136,13 +160,17 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
});
// 处理侧边栏的消息
webviewView.webview.onDidReceiveMessage((message) => {
if (message.command === "openChat") {
vscode.commands.executeCommand("ic-coder.openChat");
} else if (message.command === "login") {
vscode.commands.executeCommand("ic-coder.login");
}
});
webviewView.webview.onDidReceiveMessage(
(message) => {
if (message.command === "openChat") {
vscode.commands.executeCommand("ic-coder.openChat");
} else if (message.command === "login") {
vscode.commands.executeCommand("ic-coder.login");
}
},
undefined,
this.context.subscriptions
);
}
private getWebviewContent(

View File

@ -96,6 +96,27 @@ export function getAgentCardStyles(): string {
padding: 8px;
text-align: center;
}
/* 低调显示的工具调用样式 */
.agent-step.low-profile {
opacity: 0.5;
font-size: 10px;
padding: 2px 6px;
background: transparent;
margin-bottom: 2px;
}
.agent-step.low-profile .step-icon {
opacity: 0.4;
font-size: 10px;
}
.agent-step.low-profile .step-name {
font-weight: 300;
color: var(--vscode-descriptionForeground);
opacity: 0.7;
}
.agent-step.low-profile .step-result {
opacity: 0.6;
font-size: 9px;
}
`;
}
@ -119,14 +140,18 @@ export function getAgentCardScript(): string {
'queryKnowledgeSummary': '查询知识摘要',
'queryRules': '查询规则',
'setModule': '设置模块',
'addSignal': '添加信号',
'addSignalExample': '添加信号示例',
'addSignal': '正在分析信号定义',
'addSignalExample': '正在处理信号示例',
'validateKnowledgeGraph': '验证知识图谱',
'querySignals': '查询信号',
'addPlan': '添加计划',
'addEdge': '添加边',
'showPlan': '显示计划',
'spawnExplorer': '代码探索'
'spawnExplorer': '代码探索',
'spawnDebugger': '波形调试',
'queryByBFS': 'BFS查询',
'queryStateTransitions': '查询状态转移',
'addStateTransition': '添加状态转移'
};
return toolNameMap[toolName] || toolName;
}
@ -147,7 +172,16 @@ export function getAgentCardScript(): string {
const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄';
const displayName = getAgentToolDisplayName(step.toolName);
const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : '';
return \`<div class="agent-step"><span class="step-icon">\${icon}</span><span class="step-name">\${displayName}</span><span class="step-result">\${result}</span></div>\`;
// 为技术性工具调用添加低调样式(用户看不懂的)
const lowProfileTools = [
'knowledge_save', 'knowledge_load',
'queryKnowledgeSummary', 'queryRules', 'querySignals',
'setModule', 'addSignal', 'addSignalExample',
'validateKnowledgeGraph', 'addPlan', 'addEdge',
'showPlan', 'spawnExplorer'
];
const stepClass = lowProfileTools.includes(step.toolName) ? 'agent-step low-profile' : 'agent-step';
return \`<div class="\${stepClass}"><span class="step-icon">\${icon}</span><span class="step-name">\${displayName}</span><span class="step-result">\${result}</span></div>\`;
}).join('');
segmentDiv.innerHTML = \`

View File

@ -8,6 +8,12 @@
* - Agent: 智能体自主,自动执行大部分操作
*/
import {
plannerIconSvg,
askIconSvg,
agentIconSvg,
} from "../constants/toolIcons";
/**
* 获取模式选择器的 HTML 内容
*/
@ -23,16 +29,25 @@ export function getModeSelectorContent(): string {
</div>
<div class="mode-dropdown" id="modeDropdown">
<div class="mode-option" data-value="plan" onclick="selectMode('plan', 'Plan')">
<span class="mode-option-label">Plan</span>
<span class="mode-option-desc">只读模式</span>
<div class="mode-option-header">
<span class="mode-option-icon">${plannerIconSvg}</span>
<span class="mode-option-label">Plan</span>
</div>
<span class="mode-option-desc">仅根据需求生成设计文档,之后由用户决定下一步,可以提高工程质量</span>
</div>
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">
<span class="mode-option-label">Ask</span>
<span class="mode-option-desc">逐个确认</span>
<div class="mode-option-header">
<span class="mode-option-icon">${askIconSvg}</span>
<span class="mode-option-label">Ask</span>
</div>
<span class="mode-option-desc">仅给与智能体读权限,用于依据项目回答用户问题,或者与用户进行探讨</span>
</div>
<div class="mode-option selected" data-value="agent" onclick="selectMode('agent', 'Agent')">
<span class="mode-option-label">Agent</span>
<span class="mode-option-desc">智能体自主</span>
<div class="mode-option-header">
<span class="mode-option-icon">${agentIconSvg}</span>
<span class="mode-option-label">Agent</span>
</div>
<span class="mode-option-desc">用于快速生成工程、调试修改现有代码</span>
</div>
</div>
</div>
@ -83,7 +98,8 @@ export function getModeSelectorStyles(): string {
position: absolute;
bottom: calc(100% + 2px);
left: 0;
min-width: 140px;
min-width: 200px;
max-width: 300px;
background: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-dropdown-border);
border-radius: 4px;
@ -98,13 +114,12 @@ export function getModeSelectorStyles(): string {
/* 模式选择器的选项样式 */
.mode-option {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
align-items: flex-start;
padding: 8px 12px;
font-size: 12px;
cursor: pointer;
transition: background 0.2s ease;
white-space: nowrap;
}
.mode-option:hover {
background: rgba(128, 128, 128, 0.3);
@ -112,13 +127,31 @@ export function getModeSelectorStyles(): string {
.mode-option.selected {
background: rgba(64, 158, 255, 0.2);
}
.mode-option-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}
.mode-option-icon {
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
.mode-option-icon svg {
width: 16px;
height: 16px;
display: block;
}
.mode-option-label {
font-weight: 500;
}
.mode-option-desc {
font-size: 10px;
color: var(--vscode-descriptionForeground);
margin-left: 12px;
line-height: 1.4;
word-wrap: break-word;
white-space: normal;
}
`;
}
@ -157,9 +190,9 @@ export function getModeSelectorScript(): string {
// 更新 tooltip
if (modeTooltip) {
const tooltipMap = {
'plan': '只读模式 - 只能查询分析',
'ask': '逐个确认 - 每个写操作需确认',
'agent': '智能体自主模式','
'plan': 'plan模式',
'ask': 'ask模式',
'agent': 'agent模式'
};
modeTooltip.textContent = tooltipMap[value] || '切换模式';
}

View File

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

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

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

View File

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

View File

@ -373,6 +373,12 @@ export function getMessageAreaStyles(): string {
margin: 4px 0;
padding: 4px 0;
}
/* 低调显示的工具调用 - 移除边距和背景 */
.segment-tool.low-profile {
margin: 2px 0;
padding: 0;
background: none;
}
.tool-segment-header {
display: flex;
align-items: center;
@ -532,6 +538,23 @@ export function getMessageAreaStyles(): string {
.tool-segment-content.collapsed {
max-height: 0;
}
/* 低调显示的工具调用样式 */
.segment-tool.low-profile .tool-segment-header {
opacity: 0.65;
font-size: 11px;
}
.segment-tool.low-profile .tool-segment-icon {
opacity: 0.55;
font-size: 11px;
}
.segment-tool.low-profile .tool-segment-name {
font-weight: 300;
opacity: 0.8;
}
.segment-tool.low-profile .tool-segment-result {
opacity: 0.7;
font-size: 10px;
}
.segment-question {
background: var(--vscode-textBlockQuote-background);
border-radius: 6px;
@ -689,8 +712,8 @@ export function getMessageAreaScript(): string {
'queryKnowledgeSummary': '已查询知识摘要',
'queryRules': '已查询规则',
'setModule': '已设置模块',
'addSignal': '已添加信号',
'addSignalExample': '已添加信号示例',
'addSignal': '信号分析完成',
'addSignalExample': '信号示例处理完成',
'validateKnowledgeGraph': '已验证知识图谱',
'querySignals': '已查询信号',
'addPlan': '已添加计划',
@ -936,8 +959,30 @@ export function getMessageAreaScript(): string {
// 清空容器并重新渲染所有段落
currentSegmentedMessage.innerHTML = '';
// 合并连续相同的工具调用
const mergedSegments = [];
let i = 0;
while (i < segments.length) {
const segment = segments[i];
if (segment.type === 'tool') {
// 统计连续相同的工具调用
let count = 1;
while (i + count < segments.length &&
segments[i + count].type === 'tool' &&
segments[i + count].toolName === segment.toolName) {
count++;
}
// 添加合并后的段落(带计数)
mergedSegments.push({ ...segment, toolCount: count });
i += count;
} else {
mergedSegments.push(segment);
i++;
}
}
let toolIndex = 0; // 用于跟踪工具段落的索引
segments.forEach((segment, index) => {
mergedSegments.forEach((segment, index) => {
const segmentDiv = document.createElement('div');
segmentDiv.className = 'message-segment segment-' + segment.type;
@ -949,8 +994,23 @@ export function getMessageAreaScript(): string {
if (segment.toolName === 'spawnExplorer') {
return;
}
// 为技术性工具调用添加低调样式
const lowProfileTools = [
'knowledge_save', 'knowledge_load',
'queryKnowledgeSummary', 'queryRules', 'querySignals',
'setModule', 'addSignal', 'addSignalExample',
'validateKnowledgeGraph', 'addPlan', 'addEdge',
'showPlan', 'addRule', 'updateNode', 'addStateTransition'
];
if (lowProfileTools.includes(segment.toolName)) {
segmentDiv.className += ' low-profile';
}
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
const toolResult = segment.toolResult || '';
const toolCount = segment.toolCount || 1;
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
// 检查工具结果是否过长(超过一行显示不下)
const shouldCollapse = toolResult && toolResult.length > 60;
@ -964,7 +1024,7 @@ export function getMessageAreaScript(): string {
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) || '工具'}</span>
<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>\` : ''}
@ -1097,9 +1157,11 @@ export function getMessageAreaScript(): string {
console.log('[WebView] 对话完成,添加操作按钮');
const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions';
// 复制按钮
const copyBtn = document.createElement('button');
copyBtn.className = 'action-btn';
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
copyBtn.onclick = () => {
const textContent = segments
.filter(s => s.type === 'text' && s.content)
@ -1107,7 +1169,22 @@ export function getMessageAreaScript(): string {
.join('\\n');
copyMessage(textContent, copyBtn);
};
// 点赞按钮
const likeBtn = document.createElement('button');
likeBtn.className = 'action-btn';
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
likeBtn.onclick = () => toggleLike(likeBtn);
// 点踩按钮
const dislikeBtn = document.createElement('button');
dislikeBtn.className = 'action-btn';
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
actionsDiv.appendChild(copyBtn);
actionsDiv.appendChild(likeBtn);
actionsDiv.appendChild(dislikeBtn);
currentSegmentedMessage.appendChild(actionsDiv);
// 重置当前分段消息容器
@ -1145,7 +1222,29 @@ export function getMessageAreaScript(): string {
const container = document.createElement('div');
container.className = 'message bot-message segmented-message';
segments.forEach((segment, index) => {
// 合并连续相同的工具调用
const mergedSegments = [];
let i = 0;
while (i < segments.length) {
const segment = segments[i];
if (segment.type === 'tool') {
// 统计连续相同的工具调用
let count = 1;
while (i + count < segments.length &&
segments[i + count].type === 'tool' &&
segments[i + count].toolName === segment.toolName) {
count++;
}
// 添加合并后的段落(带计数)
mergedSegments.push({ ...segment, toolCount: count });
i += count;
} else {
mergedSegments.push(segment);
i++;
}
}
mergedSegments.forEach((segment, index) => {
const segmentDiv = document.createElement('div');
segmentDiv.className = 'message-segment segment-' + segment.type;
@ -1157,8 +1256,23 @@ export function getMessageAreaScript(): string {
if (segment.toolName === 'spawnExplorer') {
return;
}
// 为技术性工具调用添加低调样式
const lowProfileTools = [
'knowledge_save', 'knowledge_load',
'queryKnowledgeSummary', 'queryRules', 'querySignals',
'setModule', 'addSignal', 'addSignalExample',
'validateKnowledgeGraph', 'addPlan', 'addEdge',
'showPlan', 'addRule', 'updateNode', 'addStateTransition'
];
if (lowProfileTools.includes(segment.toolName)) {
segmentDiv.className += ' low-profile';
}
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
const toolResult = segment.toolResult || '';
const toolCount = segment.toolCount || 1;
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
// 检查工具结果是否过长(超过一行显示不下)
const shouldCollapse = toolResult && toolResult.length > 60;
@ -1166,7 +1280,7 @@ export function getMessageAreaScript(): string {
segmentDiv.innerHTML = \`
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
\${shouldCollapse ? \`<span class="icon-collapsed" style="display:block;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span><span class="icon-expanded" style="display:none;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span>
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
</div>
\${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
@ -1241,9 +1355,11 @@ export function getMessageAreaScript(): string {
// 添加操作按钮
const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions';
// 复制按钮
const copyBtn = document.createElement('button');
copyBtn.className = 'action-btn';
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
copyBtn.onclick = () => {
const textContent = segments
.filter(s => s.type === 'text' && s.content)
@ -1251,7 +1367,22 @@ export function getMessageAreaScript(): string {
.join('\\n');
copyMessage(textContent, copyBtn);
};
// 点赞按钮
const likeBtn = document.createElement('button');
likeBtn.className = 'action-btn';
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
likeBtn.onclick = () => toggleLike(likeBtn);
// 点踩按钮
const dislikeBtn = document.createElement('button');
dislikeBtn.className = 'action-btn';
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
actionsDiv.appendChild(copyBtn);
actionsDiv.appendChild(likeBtn);
actionsDiv.appendChild(dislikeBtn);
container.appendChild(actionsDiv);
messagesEl.appendChild(container);

View File

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

View File

@ -61,10 +61,30 @@ export function getPlanCardStyles(): string {
.plan-step:last-child {
margin-bottom: 0;
}
.step-num {
color: var(--vscode-textLink-foreground);
font-weight: 500;
margin-right: 6px;
.step-checkbox {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
margin-right: 8px;
border: 2px solid var(--vscode-textLink-foreground);
border-radius: 4px;
background: transparent;
flex-shrink: 0;
opacity: 0.6;
transition: all 0.2s ease;
}
.step-checkbox.completed {
background: var(--vscode-textLink-foreground);
border-color: var(--vscode-textLink-foreground);
opacity: 1;
}
.step-checkbox.completed::after {
content: '✓';
color: var(--vscode-editor-background);
font-size: 11px;
font-weight: bold;
}
.plan-actions {
display: flex;
@ -151,7 +171,7 @@ export function getPlanCardScript(): string {
}
const stepsHtml = (segment.planSteps || []).map((step, i) =>
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
).join('');
// 选项按钮
@ -231,7 +251,7 @@ export function getPlanCardScript(): string {
function renderPlanCardStatic(segment, segmentDiv) {
segmentDiv.className += ' segment-plan';
const stepsHtml = (segment.planSteps || []).map((step, i) =>
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
).join('');
segmentDiv.innerHTML = \`

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

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

View File

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

View File

@ -17,14 +17,27 @@ import {
getMessageAreaStyles,
getMessageAreaScript,
} from "./messageArea";
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
import {
getAgentCardStyles,
getAgentCardScript,
} from "./agentCard";
getProgressBarContent,
getProgressBarStyles,
getProgressBarScript,
} from "./progressBar";
import { getCurrentEnv } from "../config/settings";
/**
* 获取 WebView 面板的 HTML 内容
*/
export function getWebviewContent(iconUri?: string): string {
export function getWebviewContent(
iconUri?: string,
autoIconUri?: string,
liteIconUri?: string,
syIconUri?: string,
maxIconUri?: string
): string {
// 获取当前环境,只在 dev 和 test 环境下显示快速操作按钮
const currentEnv = getCurrentEnv();
const showQuickActions = currentEnv === "dev" || currentEnv === "test";
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -72,6 +85,7 @@ export function getWebviewContent(iconUri?: string): string {
${getAgentCardStyles()}
${getWaveformPreviewContent()}
${getConversationHistoryBarStyles()}
${getProgressBarStyles()}
${getInputAreaStyles()}
.file-editor-section {
@ -377,6 +391,7 @@ export function getWebviewContent(iconUri?: string): string {
</head>
<body>
${getConversationHistoryBarContent()}
${getProgressBarContent()}
<div class="header">
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
<img src="${iconUri}" alt="IC Coder" style="width: 28px; height: 28px;" />
@ -394,14 +409,18 @@ export function getWebviewContent(iconUri?: string): string {
<span id="statusText">思考中...</span>
</div>
<div class="quick-actions">
${
showQuickActions
? `<div class="quick-actions">
<button class="quick-btn" onclick="quickAction('counter')">生成计数器</button>
<button class="quick-btn" onclick="quickAction('fsm')">生成状态机</button>
<button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button>
<button class="quick-btn" onclick="quickAction('explore')">知识探索</button>
</div>
</div>`
: ""
}
${getInputAreaContent()}
${getInputAreaContent(autoIconUri, liteIconUri, syIconUri, maxIconUri)}
</div>
<script>
@ -440,10 +459,9 @@ export function getWebviewContent(iconUri?: string): string {
}
if (modeTooltip) {
const tooltipMap = {
'plan': '只读模式 - 只能查询分析',
'ask': '逐个确认 - 每个写操作需确认',
'agent': '智能体自主模式',
'auto': '完全自动 - 所有操作自动执行'
'plan': 'plan模式',
'ask': 'ask模式',
'agent': 'agent模式'
};
modeTooltip.textContent = tooltipMap[value] || '切换模式';
}
@ -571,6 +589,13 @@ export function getWebviewContent(iconUri?: string): string {
currentSegmentedMessage = null;
break;
case 'contextUsage':
// 更新上下文使用量显示
if (typeof updateContextDisplay === 'function') {
updateContextDisplay(message.currentTokens, message.maxTokens);
}
break;
case 'hideLoading':
// 隐藏加载指示器
hideLoadingIndicator();
@ -640,6 +665,10 @@ export function getWebviewContent(iconUri?: string): string {
if (messagesContainer) {
messagesContainer.innerHTML = '';
}
// 重置输入框布局到居中
if (typeof window.resetInputAreaLayout === 'function') {
window.resetInputAreaLayout();
}
break;
case 'addUserMessage':
@ -647,6 +676,10 @@ export function getWebviewContent(iconUri?: string): string {
if (message.text) {
addMessage(message.text, 'user');
}
// 检查并更新输入框布局
if (typeof window.checkMessagesAndUpdateLayout === 'function') {
window.checkMessagesAndUpdateLayout();
}
break;
case 'addAiMessage':
@ -654,6 +687,10 @@ export function getWebviewContent(iconUri?: string): string {
if (message.text) {
addMessage(message.text, 'bot');
}
// 检查并更新输入框布局
if (typeof window.checkMessagesAndUpdateLayout === 'function') {
window.checkMessagesAndUpdateLayout();
}
break;
case 'switchMode':
@ -686,6 +723,7 @@ export function getWebviewContent(iconUri?: string): string {
${getAgentCardScript()}
${getWaveformPreviewScript()}
${getConversationHistoryBarScript()}
${getProgressBarScript()}
${getInputAreaScript()}
</script></body>
</html>`;

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

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

View File

@ -0,0 +1 @@
1.3.0

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
@({{ senslist }});

View File

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

View File

@ -0,0 +1 @@
forever {{ statement }}

View File

@ -0,0 +1 @@
for({{ pre }} {{ cond }}; {{ post }}) {{ statement }}

View File

@ -0,0 +1,7 @@
function {{ retwidth }} {{ name }};
{%- for s in statement %}
{{ s }}
{%- endfor %}
endfunction

View File

@ -0,0 +1 @@
{{ name }}({% for arg in args %}{{ arg }}{% if loop.index < len_args %}, {% endif %}{% endfor %})

View File

@ -0,0 +1,4 @@
generate {% for item in items %}{{ item }}{% endfor %}
endgenerate

View File

@ -0,0 +1 @@
genvar {{ name }};

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
{% for scope in scopes %}{{ scope }}{% endfor %}

View File

@ -0,0 +1 @@
{{ name }}{%- if loop != '' %}[{{ loop }}]{%- endif %}.

View File

@ -0,0 +1,5 @@
if({{ cond }}) {{ true_statement }}
{%- if true_statement[-1] != ' ' and true_statement[-1] != '\n' %} {% endif -%}
{%- if true_statement.count('\n') == 0 and false_statement != '' %}
{% endif -%}
{%- if false_statement != '' %}else {{ false_statement }}{% endif -%}

View File

@ -0,0 +1,3 @@
initial {{ statement }}

View File

@ -0,0 +1 @@
inout {% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }}{% if dimensions != '' %} {{ dimensions }}{% endif %};

View File

@ -0,0 +1 @@
input {% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }}{% if dimensions != '' %} {{ dimensions }}{% endif %};

View File

@ -0,0 +1,5 @@
{{ name }}{{ array }}
({% for port in portlist %}
{{ port }}{%- if loop.index < len_portlist -%}, {%- endif -%}
{% endfor %}
)

View File

@ -0,0 +1,12 @@
{{ module }}
{%- if len_parameterlist > 0 %}
#({% for param in parameterlist %}
{{ param }}{%- if loop.index < len_parameterlist -%},
{%- endif -%}{% endfor %}
)
{%- endif %}
{%- for instance in instances %}
{{ instance }}{%- if loop.index < len_instances -%},
{%- endif -%}{%- endfor -%};

View File

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

View File

@ -0,0 +1 @@
integer {{ name }};

View File

@ -0,0 +1 @@
{{ first }} {% if second != '' %}{{ second }} {% endif %}{% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }}{% if dimensions != '' %} {{ dimensions }}{% endif %}

View File

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

View File

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

View File

@ -0,0 +1 @@
[{{ msb }}:{{ lsb }}]

View File

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

View File

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

View File

@ -0,0 +1 @@
localparam {% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }} = {{ value }};

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,14 @@
module {{ modulename }}{% if paramlist != '' %} #
(
{{ paramlist }}
)
{%- endif %}
(
{{ portlist }}
);
{% for item in items %}{{ item }}
{% endfor %}
endmodule

View File

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

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