Compare commits
13 Commits
feat/Plugi
...
023fdb66c3
| Author | SHA1 | Date | |
|---|---|---|---|
| 023fdb66c3 | |||
| 42481cd314 | |||
| e77194628a | |||
| 2aff54de74 | |||
| 91fadf591f | |||
| bd7a85b705 | |||
| 44bbcde5cf | |||
| d4c726ea9c | |||
| 082ef923b2 | |||
| 5cb68652f9 | |||
| 9bfa774336 | |||
| 009da59d38 | |||
| 5ea5ddba6e |
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
9
.idea/IC-Coder-Plugin.iml
generated
Normal file
9
.idea/IC-Coder-Plugin.iml
generated
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
16
.idea/codeStyles/Project.xml
generated
Normal file
16
.idea/codeStyles/Project.xml
generated
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<JSCodeStyleSettings version="0">
|
||||||
|
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||||
|
</JSCodeStyleSettings>
|
||||||
|
<JavaCodeStyleSettings>
|
||||||
|
<option name="ENABLE_JAVADOC_FORMATTING" value="false" />
|
||||||
|
</JavaCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="JavaScript">
|
||||||
|
<option name="IF_BRACE_FORCE" value="3" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="3" />
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/IC-Coder-Plugin.iml" filepath="$PROJECT_DIR$/.idea/IC-Coder-Plugin.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
90
CLAUDE.md
Normal file
90
CLAUDE.md
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
IC Coder Plugin 是一个 VS Code 扩展,为 Verilog/FPGA 开发提供智能辅助功能,包括代码生成、文件操作、iverilog 仿真和 VCD 波形查看。
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# 编译 (开发模式)
|
||||||
|
pnpm run compile
|
||||||
|
|
||||||
|
# 监听模式编译
|
||||||
|
pnpm run watch
|
||||||
|
|
||||||
|
# 生产环境打包
|
||||||
|
pnpm run package
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
pnpm run lint
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
pnpm run test
|
||||||
|
|
||||||
|
# 编译测试文件
|
||||||
|
pnpm run compile-tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
- 按 F5 在 VS Code 中启动调试模式
|
||||||
|
- 使用 webpack 打包,入口文件为 `src/extension.ts`
|
||||||
|
- 输出目录为 `dist/`
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── extension.ts # 插件入口,注册命令和视图
|
||||||
|
├── panels/
|
||||||
|
│ ├── ICHelperPanel.ts # 主聊天面板 (WebviewPanel)
|
||||||
|
│ └── VCDViewerPanel.ts # VCD 波形查看器面板
|
||||||
|
├── views/
|
||||||
|
│ ├── ICViewProvider.ts # 侧边栏视图提供者
|
||||||
|
│ └── webviewContent.ts # Webview HTML 内容 (大文件,使用搜索)
|
||||||
|
├── utils/
|
||||||
|
│ ├── messageHandler.ts # 消息处理核心逻辑 (大文件,使用搜索)
|
||||||
|
│ ├── iverilogRunner.ts # iverilog 编译和仿真执行
|
||||||
|
│ ├── chatHistoryManager.ts # 会话历史管理
|
||||||
|
│ ├── createFiles.ts # 文件创建工具
|
||||||
|
│ └── readFiles.ts # 文件读取工具
|
||||||
|
├── types/
|
||||||
|
│ └── chatHistory.ts # 消息类型定义 (LangChain4j 格式)
|
||||||
|
└── test/ # 测试文件
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
**消息流程**: Webview -> `onDidReceiveMessage` -> `messageHandler.ts` -> 后端处理 -> `panel.webview.postMessage` -> Webview
|
||||||
|
|
||||||
|
**消息类型** (`src/types/chatHistory.ts`):
|
||||||
|
- `MessageType.SYSTEM` / `USER` / `AI` / `TOOL_EXECUTION_RESULT`
|
||||||
|
- 兼容 LangChain4j 格式
|
||||||
|
|
||||||
|
**iverilog 集成** (`tools/iverilog/`):
|
||||||
|
- 内置 Windows x64 版本的 iverilog/vvp
|
||||||
|
- 通过 `IVERILOG_ROOT` 环境变量配置库路径
|
||||||
|
- 支持命令: "生成 VCD"、"运行仿真"、"生成波形"
|
||||||
|
|
||||||
|
## VS Code Extension Points
|
||||||
|
|
||||||
|
- 命令: `ic-coder.openPanel`, `ic-coder.openChat`, `ic-coder.openVCDViewer`
|
||||||
|
- 侧边栏视图: `ic-coder.mainView`
|
||||||
|
- 激活事件: `onLanguage:verilog`, `onLanguage:vhdl`, `onStartupFinished`
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `vcdrom`, `vcd-stream`, `waveql` - VCD 波形处理
|
||||||
|
- `@wavedrom/doppler`, `onml` - 波形渲染
|
||||||
|
- `iconv-lite` - 编码转换
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `webviewContent.ts` 和 `messageHandler.ts` 文件较大,建议使用搜索而非完整读取
|
||||||
|
- 当前仅支持 Windows 平台的 iverilog,其他平台需用户自行安装
|
||||||
@ -75,7 +75,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"icCoder.backendUrl": {
|
"icCoder.backendUrl": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "http://192.168.1.108:2233",
|
"default": "http://localhost:2233",
|
||||||
"description": "后端服务地址"
|
"description": "后端服务地址"
|
||||||
},
|
},
|
||||||
"icCoder.timeout": {
|
"icCoder.timeout": {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export interface IccoderConfig {
|
|||||||
/** 默认配置 */
|
/** 默认配置 */
|
||||||
const DEFAULT_CONFIG: IccoderConfig = {
|
const DEFAULT_CONFIG: IccoderConfig = {
|
||||||
backendUrl: "http://localhost:8080",
|
backendUrl: "http://localhost:8080",
|
||||||
timeout: 60000,
|
timeout: 3600000, // 1小时
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,9 @@ import {
|
|||||||
handleReplaceInFile,
|
handleReplaceInFile,
|
||||||
handleUserAnswer,
|
handleUserAnswer,
|
||||||
abortCurrentDialog,
|
abortCurrentDialog,
|
||||||
|
handlePlanAction,
|
||||||
|
setPendingPlanExecution,
|
||||||
|
getCurrentTaskId,
|
||||||
} from "../utils/messageHandler";
|
} from "../utils/messageHandler";
|
||||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||||
@ -107,7 +110,7 @@ export async function showICHelperPanel(
|
|||||||
// 切换到当前面板的任务上下文
|
// 切换到当前面板的任务上下文
|
||||||
historyManager.switchToPanelTask(panelId);
|
historyManager.switchToPanelTask(panelId);
|
||||||
|
|
||||||
handleUserMessage(panel, message.text, context.extensionPath);
|
handleUserMessage(panel, message.text, context.extensionPath, message.mode);
|
||||||
break;
|
break;
|
||||||
case "readFile":
|
case "readFile":
|
||||||
handleReadFile(panel, message.filePath);
|
handleReadFile(panel, message.filePath);
|
||||||
@ -181,6 +184,24 @@ export async function showICHelperPanel(
|
|||||||
case "abortDialog":
|
case "abortDialog":
|
||||||
abortCurrentDialog();
|
abortCurrentDialog();
|
||||||
break;
|
break;
|
||||||
|
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
||||||
|
case "planAction":
|
||||||
|
if (message.action === 'confirm') {
|
||||||
|
// 确认执行:切换到 Agent 模式
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: 'switchMode',
|
||||||
|
mode: 'agent'
|
||||||
|
});
|
||||||
|
// 获取当前会话的 taskId,用于复用知识图谱数据
|
||||||
|
const taskId = getCurrentTaskId();
|
||||||
|
if (taskId) {
|
||||||
|
// 设置待执行的计划,对话结束后自动执行(复用 taskId)
|
||||||
|
setPendingPlanExecution(panel, message.planTitle || '计划', context.extensionPath, taskId);
|
||||||
|
} else {
|
||||||
|
console.warn('[ICHelperPanel] 无法获取当前 taskId,知识图谱数据可能丢失');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import * as https from 'https';
|
|||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import { getApiUrl, getConfig } from '../config/settings';
|
import { getApiUrl, getConfig } from '../config/settings';
|
||||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse } from '../types/api';
|
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse } from '../types/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP 请求选项
|
* HTTP 请求选项
|
||||||
@ -103,6 +103,18 @@ export async function submitAnswer(answer: AnswerRequest): Promise<AnswerRespons
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交工具确认响应(Ask 模式)
|
||||||
|
* POST /api/tool/confirm
|
||||||
|
*/
|
||||||
|
export async function submitToolConfirm(response: ToolConfirmResponse): Promise<ToolResultResponse> {
|
||||||
|
console.log(`[API] 提交工具确认: confirmId=${response.confirmId}, approved=${response.approved}`);
|
||||||
|
return request<ToolResultResponse>('/api/tool/confirm', {
|
||||||
|
method: 'POST',
|
||||||
|
body: response
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 健康检查
|
* 健康检查
|
||||||
* GET /api/dialog/health
|
* GET /api/dialog/health
|
||||||
|
|||||||
@ -7,13 +7,14 @@ import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from '
|
|||||||
import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor';
|
import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor';
|
||||||
import { userInteractionManager } from './userInteraction';
|
import { userInteractionManager } from './userInteraction';
|
||||||
import { getConfig } from '../config/settings';
|
import { getConfig } from '../config/settings';
|
||||||
import type { DialogRequest, ToolCallRequest, AskUserEvent } from '../types/api';
|
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
|
||||||
|
import { submitToolConfirm, submitAnswer } from './apiClient';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息段落类型
|
* 消息段落类型
|
||||||
*/
|
*/
|
||||||
export interface MessageSegment {
|
export interface MessageSegment {
|
||||||
type: 'text' | 'tool' | 'question';
|
type: 'text' | 'tool' | 'question' | 'agent' | 'plan';
|
||||||
content?: string;
|
content?: string;
|
||||||
toolName?: string;
|
toolName?: string;
|
||||||
toolStatus?: 'running' | 'success' | 'error';
|
toolStatus?: 'running' | 'success' | 'error';
|
||||||
@ -21,6 +22,26 @@ export interface MessageSegment {
|
|||||||
askId?: string;
|
askId?: string;
|
||||||
question?: string;
|
question?: string;
|
||||||
options?: string[];
|
options?: string[];
|
||||||
|
// 智能体相关字段
|
||||||
|
agentId?: string;
|
||||||
|
agentName?: string;
|
||||||
|
agentStatus?: 'running' | 'completed' | 'error';
|
||||||
|
agentSteps?: AgentStep[];
|
||||||
|
// 计划相关字段
|
||||||
|
planTitle?: string;
|
||||||
|
planSteps?: string[];
|
||||||
|
planSummary?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体执行步骤
|
||||||
|
*/
|
||||||
|
export interface AgentStep {
|
||||||
|
step: number;
|
||||||
|
toolName: string;
|
||||||
|
toolInput?: unknown;
|
||||||
|
toolResult?: string;
|
||||||
|
status: 'running' | 'completed' | 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,6 +56,10 @@ export interface DialogCallbacks {
|
|||||||
onToolComplete?: (toolName: string, result: string) => void;
|
onToolComplete?: (toolName: string, result: string) => void;
|
||||||
/** 工具执行错误 */
|
/** 工具执行错误 */
|
||||||
onToolError?: (toolName: string, error: string) => void;
|
onToolError?: (toolName: string, error: string) => void;
|
||||||
|
/** 工具确认请求(Ask 模式) */
|
||||||
|
onToolConfirm?: (confirmId: number, toolName: string, toolInput: Record<string, unknown>) => void;
|
||||||
|
/** 计划确认请求(Plan 模式) */
|
||||||
|
onPlanConfirm?: (confirmId: number, title: string, steps: string[], summary: string) => void;
|
||||||
/** 显示问题(ask_user) */
|
/** 显示问题(ask_user) */
|
||||||
onQuestion?: (askId: string, question: string, options: string[]) => void;
|
onQuestion?: (askId: string, question: string, options: string[]) => void;
|
||||||
/** 实时更新段落(流式过程中) */
|
/** 实时更新段落(流式过程中) */
|
||||||
@ -59,8 +84,9 @@ export class DialogSession {
|
|||||||
private segments: MessageSegment[] = [];
|
private segments: MessageSegment[] = [];
|
||||||
private currentTextSegment: MessageSegment | null = null;
|
private currentTextSegment: MessageSegment | null = null;
|
||||||
|
|
||||||
constructor(extensionPath: string) {
|
constructor(extensionPath: string, existingTaskId?: string) {
|
||||||
this.taskId = generateTaskId();
|
// 支持复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||||
|
this.taskId = existingTaskId || generateTaskId();
|
||||||
this.toolContext = createToolExecutorContext(extensionPath);
|
this.toolContext = createToolExecutorContext(extensionPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,12 +152,52 @@ export class DialogSession {
|
|||||||
return this.isActive;
|
return this.isActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具操作描述(用于确认对话框)
|
||||||
|
*/
|
||||||
|
private getToolDescription(toolName: string, toolInput: Record<string, unknown>): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
switch (toolName) {
|
||||||
|
case 'file_write':
|
||||||
|
lines.push(`文件路径: ${toolInput.path || '未知'}`);
|
||||||
|
if (toolInput.content) {
|
||||||
|
const content = String(toolInput.content);
|
||||||
|
lines.push(`内容长度: ${content.length} 字符`);
|
||||||
|
lines.push(`内容预览: ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'file_delete':
|
||||||
|
lines.push(`删除文件: ${toolInput.path || '未知'}`);
|
||||||
|
break;
|
||||||
|
case 'syntax_check':
|
||||||
|
lines.push('执行语法检查');
|
||||||
|
if (toolInput.code) {
|
||||||
|
const code = String(toolInput.code);
|
||||||
|
lines.push(`代码长度: ${code.length} 字符`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'simulation':
|
||||||
|
lines.push(`RTL文件: ${toolInput.rtlPath || '未知'}`);
|
||||||
|
lines.push(`TB文件: ${toolInput.tbPath || '未知'}`);
|
||||||
|
if (toolInput.duration) {
|
||||||
|
lines.push(`仿真时长: ${toolInput.duration}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
lines.push(`参数: ${JSON.stringify(toolInput, null, 2)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送消息并开始流式对话
|
* 发送消息并开始流式对话
|
||||||
*/
|
*/
|
||||||
async sendMessage(
|
async sendMessage(
|
||||||
message: string,
|
message: string,
|
||||||
callbacks: DialogCallbacks
|
callbacks: DialogCallbacks,
|
||||||
|
mode?: RunMode
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (this.isActive) {
|
if (this.isActive) {
|
||||||
callbacks.onError?.('当前有对话正在进行中');
|
callbacks.onError?.('当前有对话正在进行中');
|
||||||
@ -148,7 +214,7 @@ export class DialogSession {
|
|||||||
taskId: this.taskId,
|
taskId: this.taskId,
|
||||||
message,
|
message,
|
||||||
userId: config.userId,
|
userId: config.userId,
|
||||||
toolMode: 'AGENT'
|
mode: mode || 'agent'
|
||||||
};
|
};
|
||||||
|
|
||||||
const sseCallbacks: SSECallbacks = {
|
const sseCallbacks: SSECallbacks = {
|
||||||
@ -164,6 +230,15 @@ export class DialogSession {
|
|||||||
onToolCall: async (data: ToolCallRequest) => {
|
onToolCall: async (data: ToolCallRequest) => {
|
||||||
const toolName = data.params.name;
|
const toolName = data.params.name;
|
||||||
console.log('[DialogSession] onToolCall:', toolName);
|
console.log('[DialogSession] onToolCall:', toolName);
|
||||||
|
|
||||||
|
// 检查是否有活跃的智能体(如果有,工具执行会显示在智能体卡片内,不需要单独显示)
|
||||||
|
const hasActiveAgent = this.segments.some(
|
||||||
|
s => s.type === 'agent' && s.agentStatus === 'running'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasActiveAgent) {
|
||||||
|
console.log('[DialogSession] onToolCall: 智能体执行中,跳过工具段落:', toolName);
|
||||||
|
} else {
|
||||||
// 检查是否已经有相同的工具段落(可能由 onToolStart 添加)
|
// 检查是否已经有相同的工具段落(可能由 onToolStart 添加)
|
||||||
const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop();
|
const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop();
|
||||||
if (lastToolSegment && lastToolSegment.toolName === toolName && lastToolSegment.toolStatus === 'running') {
|
if (lastToolSegment && lastToolSegment.toolName === toolName && lastToolSegment.toolStatus === 'running') {
|
||||||
@ -173,20 +248,25 @@ export class DialogSession {
|
|||||||
// 实时发送段落更新
|
// 实时发送段落更新
|
||||||
callbacks.onSegmentUpdate?.(this.segments);
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 注意:不在这里调用 callbacks.onToolStart,避免与 onToolStart 事件重复
|
// 注意:不在这里调用 callbacks.onToolStart,避免与 onToolStart 事件重复
|
||||||
try {
|
try {
|
||||||
await executeToolCall(data, this.toolContext);
|
await executeToolCall(data, this.toolContext);
|
||||||
|
if (!hasActiveAgent) {
|
||||||
this.updateToolSegment(toolName, 'success', '执行完成');
|
this.updateToolSegment(toolName, 'success', '执行完成');
|
||||||
// 实时发送段落更新
|
// 实时发送段落更新
|
||||||
callbacks.onSegmentUpdate?.(this.segments);
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
|
}
|
||||||
// 也不调用 callbacks.onToolComplete,避免重复
|
// 也不调用 callbacks.onToolComplete,避免重复
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : '未知错误';
|
const errorMsg = error instanceof Error ? error.message : '未知错误';
|
||||||
|
if (!hasActiveAgent) {
|
||||||
this.updateToolSegment(toolName, 'error', errorMsg);
|
this.updateToolSegment(toolName, 'error', errorMsg);
|
||||||
callbacks.onToolError?.(toolName, errorMsg);
|
|
||||||
// 实时发送段落更新
|
|
||||||
callbacks.onSegmentUpdate?.(this.segments);
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
}
|
}
|
||||||
|
callbacks.onToolError?.(toolName, errorMsg);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onToolStart: (data) => {
|
onToolStart: (data) => {
|
||||||
@ -218,6 +298,72 @@ export class DialogSession {
|
|||||||
callbacks.onSegmentUpdate?.(this.segments);
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onToolConfirm: async (data: ToolConfirmEvent) => {
|
||||||
|
console.log('[DialogSession] onToolConfirm:', data.toolName, data.confirmId);
|
||||||
|
|
||||||
|
// 调用回调通知 UI 显示确认对话框
|
||||||
|
callbacks.onToolConfirm?.(data.confirmId, data.toolName, data.toolInput);
|
||||||
|
|
||||||
|
// 使用 VSCode 快速选择框显示确认对话框
|
||||||
|
const toolDescription = this.getToolDescription(data.toolName, data.toolInput);
|
||||||
|
const result = await vscode.window.showWarningMessage(
|
||||||
|
`确认执行操作: ${data.toolName}`,
|
||||||
|
{ modal: true, detail: toolDescription },
|
||||||
|
'确认执行',
|
||||||
|
'取消'
|
||||||
|
);
|
||||||
|
|
||||||
|
const approved = result === '确认执行';
|
||||||
|
console.log('[DialogSession] 用户确认结果:', approved);
|
||||||
|
|
||||||
|
// 发送确认响应到后端
|
||||||
|
try {
|
||||||
|
await submitToolConfirm({
|
||||||
|
confirmId: data.confirmId,
|
||||||
|
taskId: this.taskId,
|
||||||
|
approved
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DialogSession] 发送确认响应失败:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onPlanConfirm: async (data: PlanConfirmEvent) => {
|
||||||
|
console.log('[DialogSession] onPlanConfirm:', data.title);
|
||||||
|
|
||||||
|
// 结束当前文本段落
|
||||||
|
this.finalizeTextSegment();
|
||||||
|
|
||||||
|
const askId = `ask_${data.confirmId}`;
|
||||||
|
|
||||||
|
// 添加计划段落到聊天界面(包含 askId 用于响应)
|
||||||
|
this.segments.push({
|
||||||
|
type: 'plan',
|
||||||
|
askId: askId,
|
||||||
|
planTitle: data.title,
|
||||||
|
planSteps: data.steps,
|
||||||
|
planSummary: data.summary
|
||||||
|
});
|
||||||
|
|
||||||
|
// 实时发送段落更新
|
||||||
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
|
|
||||||
|
// 注册问题到前端(类似 askUser),以便用户回答时能找到
|
||||||
|
const planEvent = {
|
||||||
|
askId: askId,
|
||||||
|
question: `请确认执行计划:${data.title}`,
|
||||||
|
options: ['确认执行', '修改计划', '取消']
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await userInteractionManager.handleAskUser(planEvent as AskUserEvent, this.taskId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DialogSession] 处理计划确认失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用回调通知 UI
|
||||||
|
callbacks.onPlanConfirm?.(data.confirmId, data.title, data.steps, data.summary);
|
||||||
|
},
|
||||||
|
|
||||||
onAskUser: async (data: AskUserEvent) => {
|
onAskUser: async (data: AskUserEvent) => {
|
||||||
this.finalizeTextSegment();
|
this.finalizeTextSegment();
|
||||||
this.segments.push({
|
this.segments.push({
|
||||||
@ -257,6 +403,69 @@ export class DialogSession {
|
|||||||
callbacks.onNotification?.(data.message);
|
callbacks.onNotification?.(data.message);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 智能体事件处理
|
||||||
|
onAgentStart: (data) => {
|
||||||
|
console.log('[DialogSession] onAgentStart:', data.agentId);
|
||||||
|
this.finalizeTextSegment();
|
||||||
|
this.segments.push({
|
||||||
|
type: 'agent',
|
||||||
|
agentId: data.agentId,
|
||||||
|
agentName: data.agentName,
|
||||||
|
content: data.instruction,
|
||||||
|
agentStatus: 'running',
|
||||||
|
agentSteps: []
|
||||||
|
});
|
||||||
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
|
},
|
||||||
|
|
||||||
|
onAgentProgress: (data) => {
|
||||||
|
console.log('[DialogSession] onAgentProgress:', data.agentId, data.step, data.status);
|
||||||
|
const agentSegment = this.segments.find(
|
||||||
|
s => s.type === 'agent' && s.agentId === data.agentId
|
||||||
|
);
|
||||||
|
if (agentSegment && agentSegment.agentSteps) {
|
||||||
|
if (data.status === 'running') {
|
||||||
|
agentSegment.agentSteps.push({
|
||||||
|
step: data.step,
|
||||||
|
toolName: data.toolName,
|
||||||
|
toolInput: data.toolInput,
|
||||||
|
status: 'running'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const step = agentSegment.agentSteps.find(s => s.step === data.step);
|
||||||
|
if (step) {
|
||||||
|
step.status = data.status;
|
||||||
|
step.toolResult = data.toolResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onAgentComplete: (data) => {
|
||||||
|
console.log('[DialogSession] onAgentComplete:', data.agentId);
|
||||||
|
const agentSegment = this.segments.find(
|
||||||
|
s => s.type === 'agent' && s.agentId === data.agentId
|
||||||
|
);
|
||||||
|
if (agentSegment) {
|
||||||
|
agentSegment.agentStatus = 'completed';
|
||||||
|
agentSegment.content = data.summary;
|
||||||
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onAgentError: (data) => {
|
||||||
|
console.log('[DialogSession] onAgentError:', data.agentId, data.error);
|
||||||
|
const agentSegment = this.segments.find(
|
||||||
|
s => s.type === 'agent' && s.agentId === data.agentId
|
||||||
|
);
|
||||||
|
if (agentSegment) {
|
||||||
|
agentSegment.agentStatus = 'error';
|
||||||
|
agentSegment.content = data.error;
|
||||||
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onOpen: () => {
|
onOpen: () => {
|
||||||
console.log('[DialogSession] SSE 连接已建立');
|
console.log('[DialogSession] SSE 连接已建立');
|
||||||
},
|
},
|
||||||
@ -309,13 +518,15 @@ class DialogManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建新会话
|
* 创建新会话
|
||||||
|
* @param extensionPath 扩展路径
|
||||||
|
* @param existingTaskId 可选,复用现有的 taskId(用于 Plan 模式确认后继续执行)
|
||||||
*/
|
*/
|
||||||
createSession(extensionPath: string): DialogSession {
|
createSession(extensionPath: string, existingTaskId?: string): DialogSession {
|
||||||
// 如果有活跃会话,先中止
|
// 如果有活跃会话,先中止
|
||||||
if (this.currentSession?.active) {
|
if (this.currentSession?.active) {
|
||||||
this.currentSession.abort();
|
this.currentSession.abort();
|
||||||
}
|
}
|
||||||
this.currentSession = new DialogSession(extensionPath);
|
this.currentSession = new DialogSession(extensionPath, existingTaskId);
|
||||||
return this.currentSession;
|
return this.currentSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import type {
|
|||||||
SSEEventType,
|
SSEEventType,
|
||||||
TextDeltaEvent,
|
TextDeltaEvent,
|
||||||
ToolCallRequest,
|
ToolCallRequest,
|
||||||
|
ToolConfirmEvent,
|
||||||
|
PlanConfirmEvent,
|
||||||
AskUserEvent,
|
AskUserEvent,
|
||||||
CompleteEvent,
|
CompleteEvent,
|
||||||
ErrorEvent,
|
ErrorEvent,
|
||||||
@ -21,7 +23,11 @@ import type {
|
|||||||
ToolErrorEvent,
|
ToolErrorEvent,
|
||||||
WarningEvent,
|
WarningEvent,
|
||||||
NotificationEvent,
|
NotificationEvent,
|
||||||
DepthUpdateEvent
|
DepthUpdateEvent,
|
||||||
|
AgentStartEvent,
|
||||||
|
AgentProgressEvent,
|
||||||
|
AgentCompleteEvent,
|
||||||
|
AgentErrorEvent
|
||||||
} from '../types/api';
|
} from '../types/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,6 +38,10 @@ export interface SSECallbacks {
|
|||||||
onTextDelta?: (data: TextDeltaEvent) => void;
|
onTextDelta?: (data: TextDeltaEvent) => void;
|
||||||
/** 收到工具调用请求 */
|
/** 收到工具调用请求 */
|
||||||
onToolCall?: (data: ToolCallRequest) => void;
|
onToolCall?: (data: ToolCallRequest) => void;
|
||||||
|
/** 收到工具确认请求(Ask 模式) */
|
||||||
|
onToolConfirm?: (data: ToolConfirmEvent) => void;
|
||||||
|
/** 收到计划确认请求(Plan 模式) */
|
||||||
|
onPlanConfirm?: (data: PlanConfirmEvent) => void;
|
||||||
/** 工具开始执行 */
|
/** 工具开始执行 */
|
||||||
onToolStart?: (data: ToolStartEvent) => void;
|
onToolStart?: (data: ToolStartEvent) => void;
|
||||||
/** 工具执行完成 */
|
/** 工具执行完成 */
|
||||||
@ -50,6 +60,14 @@ export interface SSECallbacks {
|
|||||||
onNotification?: (data: NotificationEvent) => void;
|
onNotification?: (data: NotificationEvent) => void;
|
||||||
/** 深度更新 */
|
/** 深度更新 */
|
||||||
onDepthUpdate?: (data: DepthUpdateEvent) => void;
|
onDepthUpdate?: (data: DepthUpdateEvent) => void;
|
||||||
|
/** 子智能体启动 */
|
||||||
|
onAgentStart?: (data: AgentStartEvent) => void;
|
||||||
|
/** 子智能体进度 */
|
||||||
|
onAgentProgress?: (data: AgentProgressEvent) => void;
|
||||||
|
/** 子智能体完成 */
|
||||||
|
onAgentComplete?: (data: AgentCompleteEvent) => void;
|
||||||
|
/** 子智能体错误 */
|
||||||
|
onAgentError?: (data: AgentErrorEvent) => void;
|
||||||
/** 连接打开 */
|
/** 连接打开 */
|
||||||
onOpen?: () => void;
|
onOpen?: () => void;
|
||||||
/** 连接关闭 */
|
/** 连接关闭 */
|
||||||
@ -124,7 +142,7 @@ export async function startStreamDialog(
|
|||||||
|
|
||||||
const body = JSON.stringify(request);
|
const body = JSON.stringify(request);
|
||||||
|
|
||||||
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, url=${urlString}`);
|
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, mode=${request.mode}, url=${urlString}`);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const options: http.RequestOptions = {
|
const options: http.RequestOptions = {
|
||||||
@ -256,6 +274,12 @@ function dispatchEvent(
|
|||||||
case 'tool_call':
|
case 'tool_call':
|
||||||
callbacks.onToolCall?.(data as ToolCallRequest);
|
callbacks.onToolCall?.(data as ToolCallRequest);
|
||||||
break;
|
break;
|
||||||
|
case 'tool_confirm':
|
||||||
|
callbacks.onToolConfirm?.(data as ToolConfirmEvent);
|
||||||
|
break;
|
||||||
|
case 'plan_confirm':
|
||||||
|
callbacks.onPlanConfirm?.(data as PlanConfirmEvent);
|
||||||
|
break;
|
||||||
case 'tool_start':
|
case 'tool_start':
|
||||||
callbacks.onToolStart?.(data as ToolStartEvent);
|
callbacks.onToolStart?.(data as ToolStartEvent);
|
||||||
break;
|
break;
|
||||||
@ -283,6 +307,18 @@ function dispatchEvent(
|
|||||||
case 'depth_update':
|
case 'depth_update':
|
||||||
callbacks.onDepthUpdate?.(data as DepthUpdateEvent);
|
callbacks.onDepthUpdate?.(data as DepthUpdateEvent);
|
||||||
break;
|
break;
|
||||||
|
case 'agent_start':
|
||||||
|
callbacks.onAgentStart?.(data as AgentStartEvent);
|
||||||
|
break;
|
||||||
|
case 'agent_progress':
|
||||||
|
callbacks.onAgentProgress?.(data as AgentProgressEvent);
|
||||||
|
break;
|
||||||
|
case 'agent_complete':
|
||||||
|
callbacks.onAgentComplete?.(data as AgentCompleteEvent);
|
||||||
|
break;
|
||||||
|
case 'agent_error':
|
||||||
|
callbacks.onAgentError?.(data as AgentErrorEvent);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.log(`[SSE] 未知事件类型: ${eventType}`, data);
|
console.log(`[SSE] 未知事件类型: ${eventType}`, data);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,10 +20,13 @@ import type {
|
|||||||
ToolName,
|
ToolName,
|
||||||
FileReadArgs,
|
FileReadArgs,
|
||||||
FileWriteArgs,
|
FileWriteArgs,
|
||||||
|
FileDeleteArgs,
|
||||||
FileListArgs,
|
FileListArgs,
|
||||||
SyntaxCheckArgs,
|
SyntaxCheckArgs,
|
||||||
SimulationArgs,
|
SimulationArgs,
|
||||||
WaveformSummaryArgs
|
WaveformSummaryArgs,
|
||||||
|
KnowledgeSaveArgs,
|
||||||
|
KnowledgeLoadArgs
|
||||||
} from '../types/api';
|
} from '../types/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,6 +64,9 @@ export async function executeToolCall(
|
|||||||
case 'file_write':
|
case 'file_write':
|
||||||
resultText = await executeFileWrite(args as unknown as FileWriteArgs);
|
resultText = await executeFileWrite(args as unknown as FileWriteArgs);
|
||||||
break;
|
break;
|
||||||
|
case 'file_delete':
|
||||||
|
resultText = await executeFileDelete(args as unknown as FileDeleteArgs);
|
||||||
|
break;
|
||||||
case 'file_list':
|
case 'file_list':
|
||||||
resultText = await executeFileList(args as unknown as FileListArgs);
|
resultText = await executeFileList(args as unknown as FileListArgs);
|
||||||
break;
|
break;
|
||||||
@ -73,6 +79,12 @@ export async function executeToolCall(
|
|||||||
case 'waveform_summary':
|
case 'waveform_summary':
|
||||||
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
|
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
|
||||||
break;
|
break;
|
||||||
|
case 'knowledge_save':
|
||||||
|
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs);
|
||||||
|
break;
|
||||||
|
case 'knowledge_load':
|
||||||
|
resultText = await executeKnowledgeLoad();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`未知工具: ${toolName}`);
|
throw new Error(`未知工具: ${toolName}`);
|
||||||
}
|
}
|
||||||
@ -105,9 +117,59 @@ async function executeFileRead(args: FileReadArgs): Promise<string> {
|
|||||||
*/
|
*/
|
||||||
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
||||||
await createOrOverwriteFile(args.path, args.content);
|
await createOrOverwriteFile(args.path, args.content);
|
||||||
|
|
||||||
|
// Verilog 文件添加知识图谱提示
|
||||||
|
const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv');
|
||||||
|
if (isVerilogFile) {
|
||||||
|
return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`;
|
||||||
|
}
|
||||||
|
|
||||||
return `文件已写入: ${args.path}`;
|
return `文件已写入: ${args.path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 file_delete 工具
|
||||||
|
* 删除指定路径的文件
|
||||||
|
*/
|
||||||
|
async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
|
||||||
|
const filePath = args.path;
|
||||||
|
|
||||||
|
// 获取工作区路径
|
||||||
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
|
throw new Error('请先打开一个工作区');
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||||
|
|
||||||
|
// 解析文件路径(支持相对路径和绝对路径)
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.join(workspacePath, filePath);
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!fs.existsSync(absolutePath)) {
|
||||||
|
throw new Error(`文件不存在: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为文件(不允许删除目录)
|
||||||
|
const stat = fs.statSync(absolutePath);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
throw new Error(`不能删除目录,请指定文件路径: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除文件
|
||||||
|
fs.unlinkSync(absolutePath);
|
||||||
|
|
||||||
|
// Verilog 文件添加知识图谱提示
|
||||||
|
const isVerilogFile = filePath.endsWith('.v') || filePath.endsWith('.sv');
|
||||||
|
if (isVerilogFile) {
|
||||||
|
return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `文件已删除: ${filePath}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行 file_list 工具
|
* 执行 file_list 工具
|
||||||
*/
|
*/
|
||||||
@ -246,6 +308,53 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string
|
|||||||
return `波形分析功能暂未实现。\n请求参数:\n- VCD文件: ${args.vcdPath}\n- 信号: ${args.signals}\n- 检查点: ${args.checkpoints || '无'}`;
|
return `波形分析功能暂未实现。\n请求参数:\n- VCD文件: ${args.vcdPath}\n- 信号: ${args.signals}\n- 检查点: ${args.checkpoints || '无'}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 knowledge_save 工具
|
||||||
|
* 保存知识图谱到 .iccoder/knowledge.json
|
||||||
|
*/
|
||||||
|
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
||||||
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
|
throw new Error('请先打开一个工作区');
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||||
|
const iccoderDir = path.join(workspacePath, '.iccoder');
|
||||||
|
const knowledgePath = path.join(iccoderDir, 'knowledge.json');
|
||||||
|
|
||||||
|
// 确保 .iccoder 目录存在
|
||||||
|
if (!fs.existsSync(iccoderDir)) {
|
||||||
|
fs.mkdirSync(iccoderDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入知识图谱
|
||||||
|
fs.writeFileSync(knowledgePath, args.data, 'utf-8');
|
||||||
|
|
||||||
|
return `知识图谱已保存: .iccoder/knowledge.json`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 knowledge_load 工具
|
||||||
|
* 从 .iccoder/knowledge.json 加载知识图谱
|
||||||
|
*/
|
||||||
|
async function executeKnowledgeLoad(): Promise<string> {
|
||||||
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
|
throw new Error('请先打开一个工作区');
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||||
|
const knowledgePath = path.join(workspacePath, '.iccoder', 'knowledge.json');
|
||||||
|
|
||||||
|
// 如果文件不存在,返回空图谱
|
||||||
|
if (!fs.existsSync(knowledgePath)) {
|
||||||
|
return JSON.stringify({ directed: true, nodes: [], links: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(knowledgePath, 'utf-8');
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 iverilog 路径
|
* 获取 iverilog 路径
|
||||||
*/
|
*/
|
||||||
|
|||||||
126
src/types/api.ts
126
src/types/api.ts
@ -5,6 +5,15 @@
|
|||||||
|
|
||||||
// ============== 对话请求/响应 ==============
|
// ============== 对话请求/响应 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行模式类型
|
||||||
|
* - plan: 只读模式,只能查询分析
|
||||||
|
* - ask: 逐个确认,每个写操作需确认
|
||||||
|
* - agent: 智能体自主(默认)
|
||||||
|
* - auto: 完全自动
|
||||||
|
*/
|
||||||
|
export type RunMode = 'plan' | 'ask' | 'agent' | 'auto';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对话请求
|
* 对话请求
|
||||||
* POST /api/dialog/stream
|
* POST /api/dialog/stream
|
||||||
@ -16,8 +25,8 @@ export interface DialogRequest {
|
|||||||
message: string;
|
message: string;
|
||||||
/** 用户ID */
|
/** 用户ID */
|
||||||
userId: string;
|
userId: string;
|
||||||
/** 工具模式 */
|
/** 运行模式 */
|
||||||
toolMode: 'ASK' | 'AGENT';
|
mode: RunMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== SSE 事件类型 ==============
|
// ============== SSE 事件类型 ==============
|
||||||
@ -26,10 +35,16 @@ export interface DialogRequest {
|
|||||||
export type SSEEventType =
|
export type SSEEventType =
|
||||||
| 'text_delta' // 文本增量
|
| 'text_delta' // 文本增量
|
||||||
| 'tool_call' // 客户端工具调用请求
|
| 'tool_call' // 客户端工具调用请求
|
||||||
|
| 'tool_confirm' // 工具确认请求(Ask 模式)
|
||||||
|
| 'plan_confirm' // 计划确认请求(Plan 模式)
|
||||||
| 'tool_start' // 工具开始执行
|
| 'tool_start' // 工具开始执行
|
||||||
| 'tool_complete' // 工具执行完成
|
| 'tool_complete' // 工具执行完成
|
||||||
| 'tool_error' // 工具执行错误
|
| 'tool_error' // 工具执行错误
|
||||||
| 'ask_user' // 向用户提问
|
| 'ask_user' // 向用户提问
|
||||||
|
| 'agent_start' // 子智能体启动
|
||||||
|
| 'agent_progress' // 子智能体进度
|
||||||
|
| 'agent_complete' // 子智能体完成
|
||||||
|
| 'agent_error' // 子智能体错误
|
||||||
| 'complete' // 对话完成
|
| 'complete' // 对话完成
|
||||||
| 'error' // 错误
|
| 'error' // 错误
|
||||||
| 'warning' // 警告
|
| 'warning' // 警告
|
||||||
@ -59,6 +74,32 @@ export interface ToolErrorEvent {
|
|||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** tool_confirm 事件数据(Ask 模式确认请求) */
|
||||||
|
export interface ToolConfirmEvent {
|
||||||
|
/** 确认ID,用于匹配响应 */
|
||||||
|
confirmId: number;
|
||||||
|
/** 工具名称 */
|
||||||
|
toolName: string;
|
||||||
|
/** 工具输入参数 */
|
||||||
|
toolInput: Record<string, unknown>;
|
||||||
|
/** 时间戳 */
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** plan_confirm 事件数据(Plan 模式计划确认) */
|
||||||
|
export interface PlanConfirmEvent {
|
||||||
|
/** 确认ID */
|
||||||
|
confirmId: number;
|
||||||
|
/** 计划标题 */
|
||||||
|
title: string;
|
||||||
|
/** 执行步骤列表 */
|
||||||
|
steps: string[];
|
||||||
|
/** 计划摘要 */
|
||||||
|
summary: string;
|
||||||
|
/** 时间戳 */
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
/** ask_user 事件数据 */
|
/** ask_user 事件数据 */
|
||||||
export interface AskUserEvent {
|
export interface AskUserEvent {
|
||||||
askId: string;
|
askId: string;
|
||||||
@ -92,6 +133,45 @@ export interface DepthUpdateEvent {
|
|||||||
depth: number;
|
depth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============== 智能体事件类型 ==============
|
||||||
|
|
||||||
|
/** agent_start 事件数据 */
|
||||||
|
export interface AgentStartEvent {
|
||||||
|
agentId: string;
|
||||||
|
agentType: string;
|
||||||
|
agentName: string;
|
||||||
|
instruction: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** agent_progress 事件数据 */
|
||||||
|
export interface AgentProgressEvent {
|
||||||
|
agentId: string;
|
||||||
|
step: number;
|
||||||
|
toolName: string;
|
||||||
|
toolInput?: unknown;
|
||||||
|
toolResult?: string;
|
||||||
|
status: 'running' | 'completed' | 'error';
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** agent_complete 事件数据 */
|
||||||
|
export interface AgentCompleteEvent {
|
||||||
|
agentId: string;
|
||||||
|
agentType: string;
|
||||||
|
summary: string;
|
||||||
|
stats: Record<string, unknown>;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** agent_error 事件数据 */
|
||||||
|
export interface AgentErrorEvent {
|
||||||
|
agentId: string;
|
||||||
|
agentType: string;
|
||||||
|
error: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
// ============== 工具调用协议 (MCP 格式) ==============
|
// ============== 工具调用协议 (MCP 格式) ==============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -186,16 +266,34 @@ export interface ToolResultResponse {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============== 工具确认响应 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具确认响应请求
|
||||||
|
* POST /api/tool/confirm
|
||||||
|
*/
|
||||||
|
export interface ToolConfirmResponse {
|
||||||
|
/** 确认ID,与 ToolConfirmEvent.confirmId 对应 */
|
||||||
|
confirmId: number;
|
||||||
|
/** 任务ID */
|
||||||
|
taskId: string;
|
||||||
|
/** 是否批准执行 */
|
||||||
|
approved: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// ============== 辅助类型 ==============
|
// ============== 辅助类型 ==============
|
||||||
|
|
||||||
/** 后端工具名称 */
|
/** 后端工具名称 */
|
||||||
export type ToolName =
|
export type ToolName =
|
||||||
| 'file_read'
|
| 'file_read'
|
||||||
| 'file_write'
|
| 'file_write'
|
||||||
|
| 'file_delete'
|
||||||
| 'file_list'
|
| 'file_list'
|
||||||
| 'syntax_check'
|
| 'syntax_check'
|
||||||
| 'simulation'
|
| 'simulation'
|
||||||
| 'waveform_summary';
|
| 'waveform_summary'
|
||||||
|
| 'knowledge_save'
|
||||||
|
| 'knowledge_load';
|
||||||
|
|
||||||
/** file_read 工具参数 */
|
/** file_read 工具参数 */
|
||||||
export interface FileReadArgs {
|
export interface FileReadArgs {
|
||||||
@ -208,6 +306,12 @@ export interface FileWriteArgs {
|
|||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** file_delete 工具参数 */
|
||||||
|
export interface FileDeleteArgs {
|
||||||
|
/** 要删除的文件路径 */
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** file_list 工具参数 */
|
/** file_list 工具参数 */
|
||||||
export interface FileListArgs {
|
export interface FileListArgs {
|
||||||
path?: string;
|
path?: string;
|
||||||
@ -233,11 +337,25 @@ export interface WaveformSummaryArgs {
|
|||||||
checkpoints?: string;
|
checkpoints?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** knowledge_save 工具参数 */
|
||||||
|
export interface KnowledgeSaveArgs {
|
||||||
|
/** 知识图谱 JSON 数据 */
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** knowledge_load 工具参数 */
|
||||||
|
export interface KnowledgeLoadArgs {
|
||||||
|
// 无参数,直接读取 .iccoder/knowledge.json
|
||||||
|
}
|
||||||
|
|
||||||
/** 工具参数联合类型 */
|
/** 工具参数联合类型 */
|
||||||
export type ToolArgs =
|
export type ToolArgs =
|
||||||
| FileReadArgs
|
| FileReadArgs
|
||||||
| FileWriteArgs
|
| FileWriteArgs
|
||||||
|
| FileDeleteArgs
|
||||||
| FileListArgs
|
| FileListArgs
|
||||||
| SyntaxCheckArgs
|
| SyntaxCheckArgs
|
||||||
| SimulationArgs
|
| SimulationArgs
|
||||||
| WaveformSummaryArgs;
|
| WaveformSummaryArgs
|
||||||
|
| KnowledgeSaveArgs
|
||||||
|
| KnowledgeLoadArgs;
|
||||||
|
|||||||
@ -19,19 +19,43 @@ import { dialogManager, DialogSession } from "../services/dialogService";
|
|||||||
import { userInteractionManager } from "../services/userInteraction";
|
import { userInteractionManager } from "../services/userInteraction";
|
||||||
import { healthCheck } from "../services/apiClient";
|
import { healthCheck } from "../services/apiClient";
|
||||||
|
|
||||||
|
import type { RunMode } from '../types/api';
|
||||||
|
|
||||||
/** 是否使用后端服务(可通过配置控制) */
|
/** 是否使用后端服务(可通过配置控制) */
|
||||||
let useBackendService = true;
|
let useBackendService = true;
|
||||||
|
|
||||||
/** 当前对话会话 */
|
/** 当前对话会话 */
|
||||||
let currentSession: DialogSession | null = null;
|
let currentSession: DialogSession | null = null;
|
||||||
|
|
||||||
|
/** 待执行的计划(Plan 模式确认后自动执行) */
|
||||||
|
let pendingPlanExecution: {
|
||||||
|
panel: vscode.WebviewPanel;
|
||||||
|
planTitle: string;
|
||||||
|
extensionPath: string;
|
||||||
|
taskId: string; // 保存 taskId 以便复用
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置待执行的计划(由 ICHelperPanel 调用)
|
||||||
|
*/
|
||||||
|
export function setPendingPlanExecution(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
planTitle: string,
|
||||||
|
extensionPath: string,
|
||||||
|
taskId: string
|
||||||
|
): void {
|
||||||
|
pendingPlanExecution = { panel, planTitle, extensionPath, taskId };
|
||||||
|
console.log('[MessageHandler] 设置待执行计划:', planTitle, 'taskId:', taskId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理用户消息
|
* 处理用户消息
|
||||||
*/
|
*/
|
||||||
export async function handleUserMessage(
|
export async function handleUserMessage(
|
||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
text: string,
|
text: string,
|
||||||
extensionPath?: string
|
extensionPath?: string,
|
||||||
|
mode?: RunMode
|
||||||
) {
|
) {
|
||||||
console.log("收到用户消息:", text);
|
console.log("收到用户消息:", text);
|
||||||
|
|
||||||
@ -63,7 +87,7 @@ export async function handleUserMessage(
|
|||||||
// 尝试使用后端服务
|
// 尝试使用后端服务
|
||||||
if (useBackendService && extensionPath) {
|
if (useBackendService && extensionPath) {
|
||||||
try {
|
try {
|
||||||
await handleUserMessageWithBackend(panel, text, extensionPath);
|
await handleUserMessageWithBackend(panel, text, extensionPath, mode);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("后端服务不可用,回退到本地模式:", error);
|
console.error("后端服务不可用,回退到本地模式:", error);
|
||||||
@ -97,11 +121,16 @@ export async function handleUserMessage(
|
|||||||
async function handleUserMessageWithBackend(
|
async function handleUserMessageWithBackend(
|
||||||
panel: vscode.WebviewPanel,
|
panel: vscode.WebviewPanel,
|
||||||
text: string,
|
text: string,
|
||||||
extensionPath: string
|
extensionPath: string,
|
||||||
|
mode?: RunMode,
|
||||||
|
reuseTaskId?: string // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// 创建或复用会话
|
// 创建或复用会话
|
||||||
if (!currentSession || !currentSession.active) {
|
if (!currentSession || !currentSession.active) {
|
||||||
currentSession = dialogManager.createSession(extensionPath);
|
currentSession = dialogManager.createSession(extensionPath, reuseTaskId);
|
||||||
|
if (reuseTaskId) {
|
||||||
|
console.log('[MessageHandler] 复用 taskId 创建会话:', reuseTaskId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const historyManager = ChatHistoryManager.getInstance();
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
@ -184,6 +213,29 @@ async function handleUserMessageWithBackend(
|
|||||||
console.warn("保存AI响应历史失败:", 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();
|
resolve();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -201,7 +253,7 @@ async function handleUserMessageWithBackend(
|
|||||||
onNotification: (message) => {
|
onNotification: (message) => {
|
||||||
vscode.window.showInformationMessage(message);
|
vscode.window.showInformationMessage(message);
|
||||||
},
|
},
|
||||||
});
|
}, mode);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,6 +278,75 @@ export function abortCurrentDialog(): void {
|
|||||||
currentSession = null;
|
currentSession = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前会话的 taskId
|
||||||
|
*/
|
||||||
|
export function getCurrentTaskId(): string | null {
|
||||||
|
return currentSession?.getTaskId() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理计划操作(Plan 模式)
|
||||||
|
* @param panel WebView 面板
|
||||||
|
* @param action 操作类型:confirm/modify/cancel
|
||||||
|
* @param planTitle 计划标题
|
||||||
|
* @param extensionPath 扩展路径
|
||||||
|
*/
|
||||||
|
export async function handlePlanAction(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
action: string,
|
||||||
|
planTitle: string,
|
||||||
|
extensionPath: string
|
||||||
|
): Promise<void> {
|
||||||
|
console.log('[handlePlanAction] action:', action, 'planTitle:', planTitle);
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'confirm':
|
||||||
|
// 确认执行:切换到 Agent 模式并发送执行消息
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: 'switchMode',
|
||||||
|
mode: 'agent'
|
||||||
|
});
|
||||||
|
// 发送执行消息
|
||||||
|
await handleUserMessage(
|
||||||
|
panel,
|
||||||
|
`请按照刚才的计划执行:${planTitle}`,
|
||||||
|
extensionPath,
|
||||||
|
'agent'
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'modify':
|
||||||
|
// 修改计划:提示用户输入修改建议
|
||||||
|
const modification = await vscode.window.showInputBox({
|
||||||
|
prompt: '请输入您对计划的修改建议',
|
||||||
|
placeHolder: '例如:第2步需要先检查文件是否存在...',
|
||||||
|
ignoreFocusOut: true
|
||||||
|
});
|
||||||
|
if (modification) {
|
||||||
|
await handleUserMessage(
|
||||||
|
panel,
|
||||||
|
`请根据以下建议修改计划:${modification}`,
|
||||||
|
extensionPath,
|
||||||
|
'plan'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cancel':
|
||||||
|
// 取消计划:通知用户
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: 'addMessage',
|
||||||
|
text: '计划已取消。',
|
||||||
|
sender: 'bot'
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn('[handlePlanAction] 未知操作:', action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析文件操作命令
|
* 解析文件操作命令
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -94,6 +94,13 @@ export async function readDirectory(
|
|||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
for (const [fileName, fileType] of entries) {
|
for (const [fileName, fileType] of entries) {
|
||||||
|
// 处理目录
|
||||||
|
if (fileType === vscode.FileType.Directory) {
|
||||||
|
results.push({ path: fileName + '/', content: '[目录]', isDirectory: true });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理文件
|
||||||
if (fileType === vscode.FileType.File) {
|
if (fileType === vscode.FileType.File) {
|
||||||
// 如果指定了扩展名过滤
|
// 如果指定了扩展名过滤
|
||||||
if (extensions && extensions.length > 0) {
|
if (extensions && extensions.length > 0) {
|
||||||
@ -108,7 +115,7 @@ export async function readDirectory(
|
|||||||
const fileUri = vscode.Uri.file(filePath);
|
const fileUri = vscode.Uri.file(filePath);
|
||||||
const contentBytes = await vscode.workspace.fs.readFile(fileUri);
|
const contentBytes = await vscode.workspace.fs.readFile(fileUri);
|
||||||
const content = Buffer.from(contentBytes).toString("utf-8");
|
const content = Buffer.from(contentBytes).toString("utf-8");
|
||||||
results.push({ path: fileName, content });
|
results.push({ path: fileName, content, isDirectory: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 跳过无法读取的文件
|
// 跳过无法读取的文件
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -47,7 +47,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
(message) => {
|
(message) => {
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
case "sendMessage":
|
case "sendMessage":
|
||||||
handleUserMessage(panel, message.text, context.extensionPath);
|
handleUserMessage(panel, message.text, context.extensionPath, message.mode);
|
||||||
break;
|
break;
|
||||||
case "readFile":
|
case "readFile":
|
||||||
handleReadFile(panel, message.filePath);
|
handleReadFile(panel, message.filePath);
|
||||||
|
|||||||
311
src/views/agentCard.ts
Normal file
311
src/views/agentCard.ts
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
/**
|
||||||
|
* 智能体卡片组件
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* - 显示子智能体的执行过程
|
||||||
|
* - 支持展开/收起步骤详情
|
||||||
|
* - 显示执行状态和统计信息
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AgentStartEvent,
|
||||||
|
AgentProgressEvent,
|
||||||
|
AgentCompleteEvent,
|
||||||
|
AgentErrorEvent
|
||||||
|
} from '../types/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能体卡片样式
|
||||||
|
*/
|
||||||
|
export function getAgentCardStyles(): string {
|
||||||
|
return `
|
||||||
|
.agent-card {
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 10px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.agent-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.agent-card-header:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
.agent-card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.agent-card-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.agent-card-status {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.agent-card-status.running {
|
||||||
|
background: var(--vscode-inputValidation-infoBackground);
|
||||||
|
color: var(--vscode-inputValidation-infoForeground);
|
||||||
|
}
|
||||||
|
.agent-card-status.completed {
|
||||||
|
background: var(--vscode-testing-iconPassed);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.agent-card-status.error {
|
||||||
|
background: var(--vscode-testing-iconFailed);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.agent-card-toggle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.agent-card-body {
|
||||||
|
padding: 12px;
|
||||||
|
border-top: 1px solid var(--vscode-input-border);
|
||||||
|
}
|
||||||
|
.agent-card-body.collapsed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.agent-card-instruction {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px dashed var(--vscode-input-border);
|
||||||
|
}
|
||||||
|
.agent-steps {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.agent-step {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 0;
|
||||||
|
border-left: 2px solid var(--vscode-input-border);
|
||||||
|
padding-left: 12px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.agent-step:last-child {
|
||||||
|
border-left-color: transparent;
|
||||||
|
}
|
||||||
|
.agent-step-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.agent-step-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.agent-step-name {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.agent-step-result {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
margin-top: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.agent-card-summary {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
border-top: 1px solid var(--vscode-input-border);
|
||||||
|
}
|
||||||
|
.agent-card-error {
|
||||||
|
color: var(--vscode-errorForeground);
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--vscode-inputValidation-errorBackground);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染智能体卡片(启动状态)
|
||||||
|
*/
|
||||||
|
export function renderAgentCardStart(event: AgentStartEvent): string {
|
||||||
|
return `
|
||||||
|
<div class="agent-card" id="agent-${event.agentId}">
|
||||||
|
<div class="agent-card-header" onclick="toggleAgentCard('${event.agentId}')">
|
||||||
|
<div class="agent-card-title">
|
||||||
|
<span class="agent-card-icon">🤖</span>
|
||||||
|
<span>${event.agentName}</span>
|
||||||
|
<span class="agent-card-status running">执行中</span>
|
||||||
|
</div>
|
||||||
|
<span class="agent-card-toggle">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="agent-card-body" id="agent-body-${event.agentId}">
|
||||||
|
<div class="agent-card-instruction">指令:${escapeHtml(event.instruction)}</div>
|
||||||
|
<div class="agent-steps" id="agent-steps-${event.agentId}">
|
||||||
|
<!-- 步骤将动态添加 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染步骤项(进行中)
|
||||||
|
*/
|
||||||
|
export function renderAgentStepRunning(event: AgentProgressEvent): string {
|
||||||
|
const inputStr = event.toolInput ? JSON.stringify(event.toolInput) : '';
|
||||||
|
const shortInput = inputStr.length > 50 ? inputStr.substring(0, 50) + '...' : inputStr;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="agent-step" id="agent-step-${event.agentId}-${event.step}">
|
||||||
|
<span class="agent-step-icon">🔄</span>
|
||||||
|
<div class="agent-step-content">
|
||||||
|
<div class="agent-step-name">${event.toolName}</div>
|
||||||
|
<div class="agent-step-result">${escapeHtml(shortInput)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新步骤项(完成)
|
||||||
|
*/
|
||||||
|
export function getStepCompleteUpdate(event: AgentProgressEvent): { icon: string; result: string } {
|
||||||
|
const result = event.toolResult || '';
|
||||||
|
const shortResult = result.length > 80 ? result.substring(0, 80) + '...' : result;
|
||||||
|
return {
|
||||||
|
icon: event.status === 'completed' ? '✅' : '❌',
|
||||||
|
result: shortResult
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能体卡片脚本
|
||||||
|
*/
|
||||||
|
export function getAgentCardScript(): string {
|
||||||
|
return `
|
||||||
|
// 智能体状态存储
|
||||||
|
const agentStates = {};
|
||||||
|
|
||||||
|
// 切换智能体卡片展开/收起
|
||||||
|
function toggleAgentCard(agentId) {
|
||||||
|
const body = document.getElementById('agent-body-' + agentId);
|
||||||
|
const header = body?.previousElementSibling;
|
||||||
|
const toggle = header?.querySelector('.agent-card-toggle');
|
||||||
|
|
||||||
|
if (body && toggle) {
|
||||||
|
body.classList.toggle('collapsed');
|
||||||
|
toggle.textContent = body.classList.contains('collapsed') ? '▶' : '▼';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理智能体启动事件
|
||||||
|
function handleAgentStart(event) {
|
||||||
|
agentStates[event.agentId] = {
|
||||||
|
status: 'running',
|
||||||
|
steps: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在当前消息中添加智能体卡片
|
||||||
|
const currentMessage = document.querySelector('.bot-message:last-child .message-content');
|
||||||
|
if (currentMessage) {
|
||||||
|
currentMessage.insertAdjacentHTML('beforeend', renderAgentCardStart(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理智能体进度事件
|
||||||
|
function handleAgentProgress(event) {
|
||||||
|
const stepsContainer = document.getElementById('agent-steps-' + event.agentId);
|
||||||
|
if (!stepsContainer) return;
|
||||||
|
|
||||||
|
if (event.status === 'running') {
|
||||||
|
// 添加新步骤
|
||||||
|
stepsContainer.insertAdjacentHTML('beforeend', renderAgentStepRunning(event));
|
||||||
|
} else if (event.status === 'completed') {
|
||||||
|
// 更新步骤状态
|
||||||
|
const stepEl = document.getElementById('agent-step-' + event.agentId + '-' + event.step);
|
||||||
|
if (stepEl) {
|
||||||
|
const iconEl = stepEl.querySelector('.agent-step-icon');
|
||||||
|
const resultEl = stepEl.querySelector('.agent-step-result');
|
||||||
|
if (iconEl) iconEl.textContent = '✅';
|
||||||
|
if (resultEl) {
|
||||||
|
const result = event.toolResult || '';
|
||||||
|
resultEl.textContent = result.length > 80 ? result.substring(0, 80) + '...' : result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理智能体完成事件
|
||||||
|
function handleAgentComplete(event) {
|
||||||
|
const card = document.getElementById('agent-' + event.agentId);
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const statusEl = card.querySelector('.agent-card-status');
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.className = 'agent-card-status completed';
|
||||||
|
statusEl.textContent = '完成';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加摘要
|
||||||
|
const body = card.querySelector('.agent-card-body');
|
||||||
|
if (body) {
|
||||||
|
body.insertAdjacentHTML('beforeend',
|
||||||
|
'<div class="agent-card-summary">' + escapeHtml(event.summary) + '</div>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动收起
|
||||||
|
body?.classList.add('collapsed');
|
||||||
|
const toggle = card.querySelector('.agent-card-toggle');
|
||||||
|
if (toggle) toggle.textContent = '▶';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理智能体错误事件
|
||||||
|
function handleAgentError(event) {
|
||||||
|
const card = document.getElementById('agent-' + event.agentId);
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const statusEl = card.querySelector('.agent-card-status');
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.className = 'agent-card-status error';
|
||||||
|
statusEl.textContent = '错误';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加错误信息
|
||||||
|
const body = card.querySelector('.agent-card-body');
|
||||||
|
if (body) {
|
||||||
|
body.insertAdjacentHTML('beforeend',
|
||||||
|
'<div class="agent-card-error">错误:' + escapeHtml(event.error) + '</div>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML 转义
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTML 转义(服务端使用)
|
||||||
|
*/
|
||||||
|
function escapeHtml(text: string): string {
|
||||||
|
if (!text) return '';
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
@ -1,6 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* 模式选择器组件
|
* 模式选择器组件
|
||||||
* 提供 Agent/Ask/Auto 三种模式的选择功能
|
* 提供 Plan/Ask/Agent/Auto 四种模式的选择功能
|
||||||
|
*
|
||||||
|
* 模式说明:
|
||||||
|
* - Plan: 只读模式,只能查询分析,不能写文件
|
||||||
|
* - Ask: 逐个确认,每个写操作需用户确认
|
||||||
|
* - Agent: 智能体自主,自动执行大部分操作
|
||||||
|
* - Auto: 完全自动,所有操作自动执行
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,12 +23,25 @@ export function getModeSelectorContent(): string {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="mode-dropdown" id="modeDropdown">
|
<div class="mode-dropdown" id="modeDropdown">
|
||||||
<div class="mode-option" data-value="agent" onclick="selectMode('agent', 'Agent')">Agent</div>
|
<div class="mode-option" data-value="plan" onclick="selectMode('plan', 'Plan')">
|
||||||
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">Ask</div>
|
<span class="mode-option-label">Plan</span>
|
||||||
<div class="mode-option" data-value="auto" onclick="selectMode('auto', 'Auto')">Auto</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>
|
||||||
|
<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>
|
||||||
|
<div class="mode-option" data-value="auto" onclick="selectMode('auto', 'Auto')">
|
||||||
|
<span class="mode-option-label">Auto</span>
|
||||||
|
<span class="mode-option-desc">完全自动</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="tooltiptext">切换模式</span>
|
</div>
|
||||||
|
<span class="tooltiptext" id="modeTooltip">智能体自主模式</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -69,7 +88,7 @@ export function getModeSelectorStyles(): string {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: calc(100% + 2px);
|
bottom: calc(100% + 2px);
|
||||||
left: 0;
|
left: 0;
|
||||||
min-width: 100%;
|
min-width: 140px;
|
||||||
background: var(--vscode-dropdown-background);
|
background: var(--vscode-dropdown-background);
|
||||||
border: 1px solid var(--vscode-dropdown-border);
|
border: 1px solid var(--vscode-dropdown-border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -83,7 +102,10 @@ export function getModeSelectorStyles(): string {
|
|||||||
}
|
}
|
||||||
/* 模式选择器的选项样式 */
|
/* 模式选择器的选项样式 */
|
||||||
.mode-option {
|
.mode-option {
|
||||||
padding: 6px 12px;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
@ -93,8 +115,15 @@ export function getModeSelectorStyles(): string {
|
|||||||
background: rgba(128, 128, 128, 0.3);
|
background: rgba(128, 128, 128, 0.3);
|
||||||
}
|
}
|
||||||
.mode-option.selected {
|
.mode-option.selected {
|
||||||
background: rgba(128, 128, 128, 0.5);
|
background: rgba(64, 158, 255, 0.2);
|
||||||
color: var(--vscode-foreground);
|
}
|
||||||
|
.mode-option-label {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.mode-option-desc {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
margin-left: 12px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -124,10 +153,23 @@ export function getModeSelectorScript(): string {
|
|||||||
function selectMode(value, label) {
|
function selectMode(value, label) {
|
||||||
currentMode = value;
|
currentMode = value;
|
||||||
const modeValue = document.getElementById('modeValue');
|
const modeValue = document.getElementById('modeValue');
|
||||||
|
const modeTooltip = document.getElementById('modeTooltip');
|
||||||
|
|
||||||
if (modeValue) {
|
if (modeValue) {
|
||||||
modeValue.textContent = label;
|
modeValue.textContent = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新 tooltip
|
||||||
|
if (modeTooltip) {
|
||||||
|
const tooltipMap = {
|
||||||
|
'plan': '只读模式 - 只能查询分析',
|
||||||
|
'ask': '逐个确认 - 每个写操作需确认',
|
||||||
|
'agent': '智能体自主模式',
|
||||||
|
'auto': '完全自动 - 所有操作自动执行'
|
||||||
|
};
|
||||||
|
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
||||||
|
}
|
||||||
|
|
||||||
// 更新选中状态
|
// 更新选中状态
|
||||||
const options = document.querySelectorAll('.mode-option');
|
const options = document.querySelectorAll('.mode-option');
|
||||||
options.forEach(option => {
|
options.forEach(option => {
|
||||||
|
|||||||
@ -261,7 +261,7 @@ export function getInputAreaStyles(): string {
|
|||||||
*/
|
*/
|
||||||
export function getInputAreaScript(): string {
|
export function getInputAreaScript(): string {
|
||||||
return `
|
return `
|
||||||
${getModeSelectorScript()}
|
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
||||||
${getModelSelectorScript()}
|
${getModelSelectorScript()}
|
||||||
${getContextButtonScript()}
|
${getContextButtonScript()}
|
||||||
${getContextCompressScript()}
|
${getContextCompressScript()}
|
||||||
@ -328,13 +328,14 @@ export function getInputAreaScript(): string {
|
|||||||
|
|
||||||
const mode = getCurrentMode(); // 从模式选择器组件获取当前模式
|
const mode = getCurrentMode(); // 从模式选择器组件获取当前模式
|
||||||
const model = getCurrentModel(); // 从模型选择器组件获取当前模型
|
const model = getCurrentModel(); // 从模型选择器组件获取当前模型
|
||||||
|
const planMode = document.getElementById('planToggle')?.checked || false;
|
||||||
|
|
||||||
addMessage(text, 'user');
|
addMessage(text, 'user');
|
||||||
|
|
||||||
// 切换按钮为暂停状态
|
// 切换按钮为暂停状态
|
||||||
setSendButtonState(true);
|
setSendButtonState(true);
|
||||||
|
|
||||||
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model });
|
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model, planMode: planMode });
|
||||||
messageInput.value = '';
|
messageInput.value = '';
|
||||||
autoResizeTextarea(); // 重置输入框高度
|
autoResizeTextarea(); // 重置输入框高度
|
||||||
messageInput.focus();
|
messageInput.focus();
|
||||||
|
|||||||
@ -528,6 +528,171 @@ export function getMessageAreaStyles(): string {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 12px;}
|
font-size: 12px;}
|
||||||
|
|
||||||
|
/* 智能体卡片样式 */
|
||||||
|
.segment-agent {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
.agent-card {
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
}
|
||||||
|
.agent-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
border-bottom: 1px solid var(--vscode-input-border);
|
||||||
|
}
|
||||||
|
.agent-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.agent-name {
|
||||||
|
font-weight: 500;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.agent-status {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.agent-status.running {
|
||||||
|
background: var(--vscode-inputValidation-infoBackground);
|
||||||
|
color: var(--vscode-inputValidation-infoForeground);
|
||||||
|
}
|
||||||
|
.agent-status.completed {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.agent-status.error {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.agent-body {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.agent-steps-container {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.agent-step {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
.agent-step:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.step-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.step-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
.step-result {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
font-size: 11px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.agent-step-placeholder {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
font-style: italic;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 计划卡片样式 */
|
||||||
|
.segment-plan {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
.plan-card {
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
}
|
||||||
|
.plan-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
border-bottom: 1px solid var(--vscode-input-border);
|
||||||
|
}
|
||||||
|
.plan-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.plan-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.plan-body {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.plan-summary {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.plan-steps {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.plan-step {
|
||||||
|
padding: 6px 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.plan-step:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.step-num {
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.plan-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border-top: 1px solid var(--vscode-input-border);
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
}
|
||||||
|
.plan-btn {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.plan-btn-confirm {
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
}
|
||||||
|
.plan-btn-confirm:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
}
|
||||||
|
.plan-btn-modify {
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
}
|
||||||
|
.plan-btn-cancel {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
|
||||||
${getWaveformPreviewContent()}
|
${getWaveformPreviewContent()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -905,6 +1070,128 @@ export function getMessageAreaScript(): string {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 支持回车提交
|
// 支持回车提交
|
||||||
|
customInput.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const customValue = customInput.value.trim();
|
||||||
|
if (customValue) {
|
||||||
|
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
} else if (segment.type === 'agent') {
|
||||||
|
// 智能体卡片渲染
|
||||||
|
segmentDiv.className += ' segment-agent';
|
||||||
|
const statusText = segment.agentStatus === 'completed' ? '完成'
|
||||||
|
: segment.agentStatus === 'error' ? '错误' : '执行中';
|
||||||
|
const statusClass = segment.agentStatus || 'running';
|
||||||
|
|
||||||
|
const stepsHtml = (segment.agentSteps || []).map(step => {
|
||||||
|
const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄';
|
||||||
|
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">\${step.toolName}</span><span class="step-result">\${result}</span></div>\`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
segmentDiv.innerHTML = \`
|
||||||
|
<div class="agent-card">
|
||||||
|
<div class="agent-header">
|
||||||
|
<span class="agent-icon">🤖</span>
|
||||||
|
<span class="agent-name">\${segment.agentName || '智能体'}</span>
|
||||||
|
<span class="agent-status \${statusClass}">\${statusText}</span>
|
||||||
|
</div>
|
||||||
|
<div class="agent-body">
|
||||||
|
<div class="agent-steps-container">
|
||||||
|
\${stepsHtml || '<div class="agent-step-placeholder">等待执行...</div>'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
|
||||||
|
// 自动滚动到最新步骤
|
||||||
|
setTimeout(() => {
|
||||||
|
const container = segmentDiv.querySelector('.agent-steps-container');
|
||||||
|
if (container) {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
} else if (segment.type === 'plan') {
|
||||||
|
// 计划卡片渲染(类似 askUser)
|
||||||
|
segmentDiv.className += ' segment-plan';
|
||||||
|
|
||||||
|
// 检查是否已回答
|
||||||
|
const isAnswered = answeredQuestions.has(segment.askId);
|
||||||
|
const selectedAnswer = answeredQuestions.get(segment.askId);
|
||||||
|
|
||||||
|
if (isAnswered) {
|
||||||
|
segmentDiv.classList.add('answered');
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||||
|
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
// 选项按钮
|
||||||
|
const options = ['确认执行', '修改计划', '取消'];
|
||||||
|
const optionsHtml = options.map(opt => {
|
||||||
|
const isSelected = isAnswered && opt === selectedAnswer;
|
||||||
|
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
segmentDiv.innerHTML = \`
|
||||||
|
<div class="plan-card">
|
||||||
|
<div class="plan-header">
|
||||||
|
<span class="plan-icon">📋</span>
|
||||||
|
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="plan-body">
|
||||||
|
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
||||||
|
<div class="plan-steps">\${stepsHtml}</div>
|
||||||
|
</div>
|
||||||
|
<div class="plan-actions">
|
||||||
|
<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>
|
||||||
|
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
|
||||||
|
<input type="text" class="custom-input" placeholder="输入修改建议..." />
|
||||||
|
<button class="custom-submit">提交</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
|
||||||
|
// 只在未回答时添加事件监听
|
||||||
|
if (!isAnswered) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const optionButtons = segmentDiv.querySelectorAll('.question-option');
|
||||||
|
optionButtons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const option = this.getAttribute('data-option');
|
||||||
|
// 发送答案到后端
|
||||||
|
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
|
||||||
|
// 同时发送 planAction 用于模式切换
|
||||||
|
const actionMap = {
|
||||||
|
'确认执行': 'confirm',
|
||||||
|
'修改计划': 'modify',
|
||||||
|
'取消': 'cancel'
|
||||||
|
};
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'planAction',
|
||||||
|
action: actionMap[option] || option,
|
||||||
|
planTitle: segment.planTitle
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||||
|
const customInput = segmentDiv.querySelector('.custom-input');
|
||||||
|
if (submitBtn && customInput) {
|
||||||
|
submitBtn.addEventListener('click', function() {
|
||||||
|
const customValue = customInput.value.trim();
|
||||||
|
if (customValue) {
|
||||||
|
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
customInput.addEventListener('keypress', function(e) {
|
customInput.addEventListener('keypress', function(e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
const customValue = customInput.value.trim();
|
const customValue = customInput.value.trim();
|
||||||
@ -1058,6 +1345,74 @@ export function getMessageAreaScript(): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
\`;
|
\`;
|
||||||
|
} else if (segment.type === 'agent') {
|
||||||
|
// 智能体卡片渲染
|
||||||
|
segmentDiv.className += ' segment-agent';
|
||||||
|
const statusText = segment.agentStatus === 'completed' ? '完成'
|
||||||
|
: segment.agentStatus === 'error' ? '错误' : '执行中';
|
||||||
|
const statusClass = segment.agentStatus || 'running';
|
||||||
|
|
||||||
|
const stepsHtml = (segment.agentSteps || []).map(step => {
|
||||||
|
const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄';
|
||||||
|
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">\${step.toolName}</span><span class="step-result">\${result}</span></div>\`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
segmentDiv.innerHTML = \`
|
||||||
|
<div class="agent-card">
|
||||||
|
<div class="agent-header">
|
||||||
|
<span class="agent-icon">🤖</span>
|
||||||
|
<span class="agent-name">\${segment.agentName || '智能体'}</span>
|
||||||
|
<span class="agent-status \${statusClass}">\${statusText}</span>
|
||||||
|
</div>
|
||||||
|
<div class="agent-body">
|
||||||
|
<div class="agent-steps-container">
|
||||||
|
\${stepsHtml || '<div class="agent-step-placeholder">等待执行...</div>'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
} else if (segment.type === 'plan') {
|
||||||
|
// 计划卡片渲染
|
||||||
|
segmentDiv.className += ' segment-plan';
|
||||||
|
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||||
|
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
segmentDiv.innerHTML = \`
|
||||||
|
<div class="plan-card">
|
||||||
|
<div class="plan-header">
|
||||||
|
<span class="plan-icon">📋</span>
|
||||||
|
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="plan-body">
|
||||||
|
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
||||||
|
<div class="plan-steps">\${stepsHtml}</div>
|
||||||
|
</div>
|
||||||
|
<div class="plan-actions">
|
||||||
|
<button class="plan-btn plan-btn-confirm" data-action="confirm">确认执行</button>
|
||||||
|
<button class="plan-btn plan-btn-modify" data-action="modify">修改计划</button>
|
||||||
|
<button class="plan-btn plan-btn-cancel" data-action="cancel">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
|
||||||
|
// 绑定按钮事件
|
||||||
|
setTimeout(() => {
|
||||||
|
const planCard = segmentDiv.querySelector('.plan-card');
|
||||||
|
if (planCard) {
|
||||||
|
planCard.querySelectorAll('.plan-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const action = e.currentTarget?.dataset?.action;
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'planAction',
|
||||||
|
action: action,
|
||||||
|
planTitle: segment.planTitle
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
container.appendChild(segmentDiv);
|
container.appendChild(segmentDiv);
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* Plan 开关组件
|
* Plan 开关组件
|
||||||
|
* 注意:功能已移至模式选择器,此组件仅保留样式(已禁用)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 Plan 开关的 HTML 内容
|
* 获取 Plan 开关的 HTML 内容
|
||||||
|
* 已禁用,仅保留样式展示
|
||||||
*/
|
*/
|
||||||
export function getPlanToggleContent(): string {
|
export function getPlanToggleContent(): string {
|
||||||
return `
|
return `
|
||||||
<div class="tooltip">
|
<div class="tooltip">
|
||||||
<label class="plan-toggle">
|
<label class="plan-toggle plan-toggle-disabled">
|
||||||
<input type="checkbox" id="planToggle" onchange="handlePlanToggle()">
|
<input type="checkbox" id="planToggle" disabled>
|
||||||
<span class="plan-toggle-slider"></span>
|
<span class="plan-toggle-slider"></span>
|
||||||
<span class="plan-toggle-label">Plan</span>
|
<span class="plan-toggle-label">Plan</span>
|
||||||
</label>
|
</label>
|
||||||
<span class="tooltiptext" id="planTooltip">启用 Plan 模式</span>
|
<span class="tooltiptext" id="planTooltip">请使用模式选择器切换 Plan 模式</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -73,6 +75,17 @@ export function getPlanToggleStyles(): string {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--vscode-foreground);
|
color: var(--vscode-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 禁用状态样式 */
|
||||||
|
.plan-toggle-disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-toggle-disabled .plan-toggle-slider {
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
border-color: var(--vscode-input-border);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,10 @@ import {
|
|||||||
getMessageAreaStyles,
|
getMessageAreaStyles,
|
||||||
getMessageAreaScript,
|
getMessageAreaScript,
|
||||||
} from "./messageArea";
|
} from "./messageArea";
|
||||||
|
import {
|
||||||
|
getAgentCardStyles,
|
||||||
|
getAgentCardScript,
|
||||||
|
} from "./agentCard";
|
||||||
/**
|
/**
|
||||||
* 获取 WebView 面板的 HTML 内容
|
* 获取 WebView 面板的 HTML 内容
|
||||||
*/
|
*/
|
||||||
@ -65,6 +69,7 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
padding: 0 20px 20px 20px;
|
padding: 0 20px 20px 20px;
|
||||||
}
|
}
|
||||||
${getMessageAreaStyles()}
|
${getMessageAreaStyles()}
|
||||||
|
${getAgentCardStyles()}
|
||||||
${getWaveformPreviewContent()}
|
${getWaveformPreviewContent()}
|
||||||
${getConversationHistoryBarStyles()}
|
${getConversationHistoryBarStyles()}
|
||||||
${getInputAreaStyles()}
|
${getInputAreaStyles()}
|
||||||
@ -393,6 +398,7 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
<button class="quick-btn" onclick="quickAction('counter')">生成计数器</button>
|
<button class="quick-btn" onclick="quickAction('counter')">生成计数器</button>
|
||||||
<button class="quick-btn" onclick="quickAction('fsm')">生成状态机</button>
|
<button class="quick-btn" onclick="quickAction('fsm')">生成状态机</button>
|
||||||
<button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button>
|
<button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button>
|
||||||
|
<button class="quick-btn" onclick="quickAction('explore')">知识探索</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${getInputAreaContent()}
|
${getInputAreaContent()}
|
||||||
@ -411,11 +417,68 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
let loadingIndicator = null;
|
let loadingIndicator = null;
|
||||||
let currentSegmentedMessage = null; // 当前分段消息容器
|
let currentSegmentedMessage = null; // 当前分段消息容器
|
||||||
|
|
||||||
|
// ========== 模式选择器脚本(直接内联,避免模板字符串嵌套问题)==========
|
||||||
|
let currentMode = 'agent';
|
||||||
|
|
||||||
|
function toggleModeDropdown() {
|
||||||
|
const modeSelectEl = document.getElementById('modeSelect');
|
||||||
|
const modelSelectEl = document.getElementById('modelSelect');
|
||||||
|
if (modeSelectEl) {
|
||||||
|
modeSelectEl.classList.toggle('active');
|
||||||
|
if (modelSelectEl) {
|
||||||
|
modelSelectEl.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectMode(value, label) {
|
||||||
|
currentMode = value;
|
||||||
|
const modeValue = document.getElementById('modeValue');
|
||||||
|
const modeTooltip = document.getElementById('modeTooltip');
|
||||||
|
if (modeValue) {
|
||||||
|
modeValue.textContent = label;
|
||||||
|
}
|
||||||
|
if (modeTooltip) {
|
||||||
|
const tooltipMap = {
|
||||||
|
'plan': '只读模式 - 只能查询分析',
|
||||||
|
'ask': '逐个确认 - 每个写操作需确认',
|
||||||
|
'agent': '智能体自主模式',
|
||||||
|
'auto': '完全自动 - 所有操作自动执行'
|
||||||
|
};
|
||||||
|
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
||||||
|
}
|
||||||
|
const options = document.querySelectorAll('.mode-option');
|
||||||
|
options.forEach(option => {
|
||||||
|
if (option.getAttribute('data-value') === value) {
|
||||||
|
option.classList.add('selected');
|
||||||
|
} else {
|
||||||
|
option.classList.remove('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const modeSelectEl = document.getElementById('modeSelect');
|
||||||
|
if (modeSelectEl) {
|
||||||
|
modeSelectEl.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentMode() {
|
||||||
|
return currentMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
const modeSelectEl = document.getElementById('modeSelect');
|
||||||
|
if (modeSelectEl && !modeSelectEl.contains(event.target)) {
|
||||||
|
modeSelectEl.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// ========== 模式选择器脚本结束 ==========
|
||||||
|
|
||||||
function quickAction(type) {
|
function quickAction(type) {
|
||||||
const questions = {
|
const questions = {
|
||||||
counter: '生成一个4位同步计数器',
|
counter: '生成一个4位同步计数器',
|
||||||
fsm: '生成一个状态机',
|
fsm: '生成一个状态机',
|
||||||
testbench: '生成测试平台'
|
testbench: '生成测试平台',
|
||||||
|
explore: '请启动知识探索智能体,分析当前项目结构'
|
||||||
};
|
};
|
||||||
if (questions[type]) {
|
if (questions[type]) {
|
||||||
messageInput.value = questions[type];
|
messageInput.value = questions[type];
|
||||||
@ -581,12 +644,34 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'switchMode':
|
||||||
|
// 切换运行模式(Plan 确认后自动切换到 Agent)
|
||||||
|
if (message.mode && typeof selectMode === 'function') {
|
||||||
|
const labelMap = {
|
||||||
|
'plan': 'Plan',
|
||||||
|
'ask': 'Ask',
|
||||||
|
'agent': 'Agent',
|
||||||
|
'auto': 'Auto'
|
||||||
|
};
|
||||||
|
selectMode(message.mode, labelMap[message.mode] || message.mode);
|
||||||
|
console.log('[WebView] 模式已切换到:', message.mode);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'addMessage':
|
||||||
|
// 添加消息(通用)
|
||||||
|
if (message.text && message.sender) {
|
||||||
|
addMessage(message.text, message.sender);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('[WebView] 未处理的消息类型:', message.command);
|
console.log('[WebView] 未处理的消息类型:', message.command);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
${getMessageAreaScript()}
|
${getMessageAreaScript()}
|
||||||
|
${getAgentCardScript()}
|
||||||
${getWaveformPreviewScript()}
|
${getWaveformPreviewScript()}
|
||||||
${getConversationHistoryBarScript()}
|
${getConversationHistoryBarScript()}
|
||||||
${getInputAreaScript()}
|
${getInputAreaScript()}
|
||||||
|
|||||||
Reference in New Issue
Block a user