Compare commits
8 Commits
b676846b2f
...
318d3964bd
| Author | SHA1 | Date | |
|---|---|---|---|
| 318d3964bd | |||
| 770da72ce3 | |||
| c050f0e167 | |||
| 3daa66ea01 | |||
| 9bdaf34471 | |||
| 25a8ea5aa4 | |||
| ef83016b7f | |||
| 2e6812d00d |
751
docs/会话存储技术文档.md
Normal file
751
docs/会话存储技术文档.md
Normal file
@ -0,0 +1,751 @@
|
|||||||
|
# IC Coder 会话存储技术文档
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
IC Coder 的会话存储系统负责持久化保存用户与 AI 的对话历史,支持多项目、多任务的会话管理。系统采用文件系统存储方案,将会话数据按项目和任务组织,便于管理和检索。
|
||||||
|
|
||||||
|
### 1.1 核心特性
|
||||||
|
|
||||||
|
- **多项目支持**:不同项目的会话数据独立存储
|
||||||
|
- **任务级管理**:每个会话作为独立任务进行管理
|
||||||
|
- **分页加载**:支持历史会话的分页查询,提升性能
|
||||||
|
- **实时更新**:会话数据实时保存,防止数据丢失
|
||||||
|
- **统计信息**:记录 Token 使用量、对话轮次等统计数据
|
||||||
|
|
||||||
|
### 1.2 技术栈
|
||||||
|
|
||||||
|
- **存储方式**:文件系统(JSON/JSONL 格式)
|
||||||
|
- **存储位置**:`~/.iccoder/projects/{项目路径编码}/{taskId}/`
|
||||||
|
- **数据格式**:
|
||||||
|
- `meta.json`:任务元数据
|
||||||
|
- `conversation.json`:完整对话历史
|
||||||
|
- `conversation_meta.jsonl`:对话轮次元数据(JSONL 格式)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
### 2.1 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.iccoder/
|
||||||
|
└── projects/
|
||||||
|
└── {项目路径编码}/
|
||||||
|
└── {taskId}/
|
||||||
|
├── meta.json # 任务元数据
|
||||||
|
├── conversation.json # 对话历史
|
||||||
|
└── conversation_meta.jsonl # 对话元数据
|
||||||
|
```
|
||||||
|
|
||||||
|
**项目路径编码规则**:
|
||||||
|
- 移除冒号 `:`
|
||||||
|
- 将斜杠 `/` 和反斜杠 `\` 替换为 `--`
|
||||||
|
- 示例:`C:\Users\admin\Documents\Project` → `C--Users--admin--Documents--Project`
|
||||||
|
|
||||||
|
**任务 ID 格式**:
|
||||||
|
- 格式:`task_{date}_{sequence}`
|
||||||
|
- 示例:`task_20231226_a3f9k2`
|
||||||
|
- `date`:8 位日期(YYYYMMDD)
|
||||||
|
- `sequence`:6 位随机字符串
|
||||||
|
|
||||||
|
### 2.2 核心类:ChatHistoryManager
|
||||||
|
|
||||||
|
`ChatHistoryManager` 是会话存储的核心管理类,采用单例模式设计。
|
||||||
|
|
||||||
|
**主要职责**:
|
||||||
|
1. 管理会话存储目录
|
||||||
|
2. 创建和切换任务
|
||||||
|
3. 保存和加载对话历史
|
||||||
|
4. 记录统计信息
|
||||||
|
5. 提供会话历史查询接口
|
||||||
|
|
||||||
|
**关键属性**:
|
||||||
|
```typescript
|
||||||
|
private static instance: ChatHistoryManager;
|
||||||
|
private baseDir: string; // ~/.iccoder
|
||||||
|
private currentTaskId: string | null; // 当前任务 ID
|
||||||
|
private currentProjectPath: string | null; // 当前项目路径
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 数据模型
|
||||||
|
|
||||||
|
### 3.1 TaskMeta(任务元数据)
|
||||||
|
|
||||||
|
存储在 `meta.json` 文件中,记录任务的基本信息和统计数据。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TaskMeta {
|
||||||
|
taskId: string; // 任务 ID
|
||||||
|
taskName: string; // 任务名称
|
||||||
|
projectPath: string; // 项目路径
|
||||||
|
createdAt: string; // 创建时间(ISO 8601)
|
||||||
|
updatedAt: string; // 更新时间(ISO 8601)
|
||||||
|
stats: {
|
||||||
|
credits: number; // 消耗的积分
|
||||||
|
totalTokens: number; // 总 Token 数
|
||||||
|
inputTokens: number; // 输入 Token 数
|
||||||
|
outputTokens: number; // 输出 Token 数
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"taskId": "task_20231226_a3f9k2",
|
||||||
|
"taskName": "实现计数器功能",
|
||||||
|
"projectPath": "C:\\Users\\admin\\Documents\\Project",
|
||||||
|
"createdAt": "2023-12-26T10:30:00.000Z",
|
||||||
|
"updatedAt": "2023-12-26T11:45:00.000Z",
|
||||||
|
"stats": {
|
||||||
|
"credits": 0,
|
||||||
|
"totalTokens": 15420,
|
||||||
|
"inputTokens": 8200,
|
||||||
|
"outputTokens": 7220
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 ChatMessage(对话消息)
|
||||||
|
|
||||||
|
存储在 `conversation.json` 文件中,记录完整的对话历史。
|
||||||
|
|
||||||
|
**消息类型枚举**:
|
||||||
|
```typescript
|
||||||
|
enum MessageType {
|
||||||
|
USER = "USER", // 用户消息
|
||||||
|
AI = "AI", // AI 消息
|
||||||
|
SYSTEM = "SYSTEM", // 系统消息
|
||||||
|
TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT" // 工具执行结果
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**用户消息**:
|
||||||
|
```typescript
|
||||||
|
interface UserMessage {
|
||||||
|
type: MessageType.USER;
|
||||||
|
contents: Array<{
|
||||||
|
type: "TEXT";
|
||||||
|
text: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**AI 消息**:
|
||||||
|
```typescript
|
||||||
|
interface AiMessage {
|
||||||
|
type: MessageType.AI;
|
||||||
|
text: string;
|
||||||
|
toolExecutionRequests?: Array<{
|
||||||
|
id: string;
|
||||||
|
toolName: string;
|
||||||
|
parameters: any;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**系统消息**:
|
||||||
|
```typescript
|
||||||
|
interface SystemMessage {
|
||||||
|
type: MessageType.SYSTEM;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**工具执行结果消息**:
|
||||||
|
```typescript
|
||||||
|
interface ToolExecutionResultMessage {
|
||||||
|
type: MessageType.TOOL_EXECUTION_RESULT;
|
||||||
|
id: string;
|
||||||
|
toolName: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 ConversationMeta(对话轮次元数据)
|
||||||
|
|
||||||
|
存储在 `conversation_meta.jsonl` 文件中,每行一条记录(JSONL 格式)。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ConversationMeta {
|
||||||
|
turnId: number; // 对话轮次 ID
|
||||||
|
timestamp: string; // 时间戳(ISO 8601)
|
||||||
|
usage?: {
|
||||||
|
inputTokens?: number;
|
||||||
|
outputTokens?: number;
|
||||||
|
totalTokens?: number;
|
||||||
|
};
|
||||||
|
model?: string; // 使用的模型
|
||||||
|
duration?: number; // 耗时(毫秒)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```jsonl
|
||||||
|
{"turnId":1,"timestamp":"2023-12-26T10:30:15.000Z","usage":{"inputTokens":120,"outputTokens":350,"totalTokens":470},"model":"gpt-4","duration":2500}
|
||||||
|
{"turnId":2,"timestamp":"2023-12-26T10:32:30.000Z","usage":{"inputTokens":200,"outputTokens":450,"totalTokens":650},"model":"gpt-4","duration":3200}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 核心功能实现
|
||||||
|
|
||||||
|
### 4.1 任务创建
|
||||||
|
|
||||||
|
**方法**:`createTask(projectPath: string, taskName: string): Promise<TaskMeta>`
|
||||||
|
|
||||||
|
**流程**:
|
||||||
|
1. 生成唯一的任务 ID
|
||||||
|
2. 创建任务元数据对象
|
||||||
|
3. 创建任务目录
|
||||||
|
4. 保存 `meta.json`
|
||||||
|
5. 初始化空的 `conversation.json`
|
||||||
|
6. 设置为当前任务
|
||||||
|
|
||||||
|
**代码位置**:`chatHistoryManager.ts:114-146`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
public async createTask(projectPath: string, taskName: string): Promise<TaskMeta> {
|
||||||
|
const taskId = this.generateTaskId();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const meta: TaskMeta = {
|
||||||
|
taskId,
|
||||||
|
taskName,
|
||||||
|
projectPath,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
stats: {
|
||||||
|
credits: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
inputTokens: 0,
|
||||||
|
outputTokens: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.currentTaskId = taskId;
|
||||||
|
this.currentProjectPath = projectPath;
|
||||||
|
|
||||||
|
// 创建任务目录
|
||||||
|
const taskDir = this.getTaskDir(projectPath, taskId);
|
||||||
|
await this.ensureTaskDir(taskDir);
|
||||||
|
|
||||||
|
// 保存 meta.json
|
||||||
|
await this.saveTaskMeta(meta);
|
||||||
|
|
||||||
|
// 初始化空的 conversation.json
|
||||||
|
await this.saveConversation([]);
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 消息保存
|
||||||
|
|
||||||
|
系统提供了四种消息保存方法:
|
||||||
|
|
||||||
|
#### 4.2.1 添加用户消息
|
||||||
|
|
||||||
|
**方法**:`addUserMessage(text: string): Promise<void>`
|
||||||
|
|
||||||
|
**代码位置**:`chatHistoryManager.ts:285-299`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
public async addUserMessage(text: string): Promise<void> {
|
||||||
|
await this.ensureCurrentTask();
|
||||||
|
const messages = await this.loadConversation();
|
||||||
|
|
||||||
|
const userMessage: UserMessage = {
|
||||||
|
type: MessageType.USER,
|
||||||
|
contents: [{ type: "TEXT", text }]
|
||||||
|
};
|
||||||
|
|
||||||
|
messages.push(userMessage);
|
||||||
|
await this.saveConversation(messages);
|
||||||
|
|
||||||
|
// 更新任务元数据
|
||||||
|
await this.updateTaskTimestamp();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2.2 添加 AI 消息
|
||||||
|
|
||||||
|
**方法**:`addAiMessage(text: string, toolRequests?: any[]): Promise<void>`
|
||||||
|
|
||||||
|
**代码位置**:`chatHistoryManager.ts:304-319`
|
||||||
|
|
||||||
|
#### 4.2.3 添加系统消息
|
||||||
|
|
||||||
|
**方法**:`addSystemMessage(text: string): Promise<void>`
|
||||||
|
|
||||||
|
**代码位置**:`chatHistoryManager.ts:324-335`
|
||||||
|
|
||||||
|
#### 4.2.4 添加工具执行结果
|
||||||
|
|
||||||
|
**方法**:`addToolExecutionResult(id: string, toolName: string, result: string): Promise<void>`
|
||||||
|
|
||||||
|
**代码位置**:`chatHistoryManager.ts:340-353`
|
||||||
|
|
||||||
|
### 4.3 对话元数据记录
|
||||||
|
|
||||||
|
**方法**:`recordTurnMeta(turnId, usage?, model?, duration?): Promise<void>`
|
||||||
|
|
||||||
|
**功能**:记录每轮对话的元数据,包括 Token 使用量、模型信息、耗时等。
|
||||||
|
|
||||||
|
**代码位置**:`chatHistoryManager.ts:358-378`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
public async recordTurnMeta(
|
||||||
|
turnId: number,
|
||||||
|
usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number },
|
||||||
|
model?: string,
|
||||||
|
duration?: number
|
||||||
|
): Promise<void> {
|
||||||
|
const meta: ConversationMeta = {
|
||||||
|
turnId,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
usage,
|
||||||
|
model,
|
||||||
|
duration
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.appendConversationMeta(meta);
|
||||||
|
|
||||||
|
// 更新任务统计
|
||||||
|
if (usage) {
|
||||||
|
await this.updateTaskStats(usage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 会话历史查询
|
||||||
|
|
||||||
|
**方法**:`getConversationHistoryList(projectPath, offset, limit): Promise<{items, total, hasMore}>`
|
||||||
|
|
||||||
|
**功能**:分页查询项目的会话历史列表。
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
- `projectPath`:项目路径
|
||||||
|
- `offset`:偏移量(从第几条开始,默认 0)
|
||||||
|
- `limit`:每页数量(默认 10)
|
||||||
|
|
||||||
|
**返回值**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
items: Array<{
|
||||||
|
id: string; // 任务 ID
|
||||||
|
title: string; // 会话标题(第一句用户消息)
|
||||||
|
timestamp: string; // 创建时间
|
||||||
|
}>;
|
||||||
|
total: number; // 总数
|
||||||
|
hasMore: boolean; // 是否还有更多
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**代码位置**:`chatHistoryManager.ts:525-590`
|
||||||
|
|
||||||
|
**实现逻辑**:
|
||||||
|
1. 获取项目的所有任务列表(按更新时间倒序)
|
||||||
|
2. 根据 offset 和 limit 进行分页
|
||||||
|
3. 读取每个任务的 `conversation.json`
|
||||||
|
4. 提取第一条用户消息作为标题(截取前 50 个字符)
|
||||||
|
5. 返回分页结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 前端集成
|
||||||
|
|
||||||
|
### 5.1 会话历史栏组件
|
||||||
|
|
||||||
|
**文件**:`conversationHistoryBar.ts`
|
||||||
|
|
||||||
|
**组件结构**:
|
||||||
|
- 下拉按钮:显示 "Past Conversations"
|
||||||
|
- 下拉菜单:显示会话历史列表
|
||||||
|
- 新建按钮:创建新会话
|
||||||
|
|
||||||
|
**关键功能**:
|
||||||
|
|
||||||
|
#### 5.1.1 加载会话历史
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function loadMoreHistory() {
|
||||||
|
if (isLoadingHistory || (currentOffset > 0 && !hasMoreHistory)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已达到最大数量(100 条)
|
||||||
|
if (currentOffset >= MAX_HISTORY_ITEMS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingHistory = true;
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'loadConversationHistory',
|
||||||
|
offset: currentOffset,
|
||||||
|
limit: HISTORY_PAGE_SIZE
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.1.2 渲染会话列表
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function renderConversationHistory(data) {
|
||||||
|
isLoadingHistory = false;
|
||||||
|
|
||||||
|
// 追加新数据
|
||||||
|
conversationHistory = conversationHistory.concat(data.items);
|
||||||
|
totalHistory = data.total;
|
||||||
|
hasMoreHistory = data.hasMore;
|
||||||
|
currentOffset += data.items.length;
|
||||||
|
|
||||||
|
// 渲染所有历史记录
|
||||||
|
historyList.innerHTML = conversationHistory.map(item => `
|
||||||
|
<div class="history-item" onclick="selectConversation('${item.id}')">
|
||||||
|
<div class="history-item-title">${item.title || '未命名会话'}</div>
|
||||||
|
<div class="history-item-time">${formatTime(item.timestamp)}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// 如果还有更多数据,添加"加载更多"提示
|
||||||
|
if (hasMoreHistory && currentOffset < MAX_HISTORY_ITEMS) {
|
||||||
|
historyList.innerHTML += `
|
||||||
|
<div class="history-load-more">
|
||||||
|
<span>滚动加载更多...</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.1.3 滚动加载
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
historyDropdownMenu.addEventListener('scroll', () => {
|
||||||
|
const menu = historyDropdownMenu;
|
||||||
|
const scrollTop = menu.scrollTop;
|
||||||
|
const scrollHeight = menu.scrollHeight;
|
||||||
|
const clientHeight = menu.clientHeight;
|
||||||
|
|
||||||
|
// 当滚动到距离底部 50px 时,加载更多
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 50) {
|
||||||
|
loadMoreHistory();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.1.4 时间格式化
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function formatTime(timestamp) {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now - date;
|
||||||
|
|
||||||
|
if (diff < 60000) return '刚刚';
|
||||||
|
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
|
||||||
|
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
|
||||||
|
if (diff < 604800000) return Math.floor(diff / 86400000) + '天前';
|
||||||
|
|
||||||
|
// 超过7天显示具体日期
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 后端消息处理
|
||||||
|
|
||||||
|
**文件**:`ICHelperPanel.ts`
|
||||||
|
|
||||||
|
**消息处理流程**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
case "loadConversationHistory":
|
||||||
|
// 加载会话历史(支持分页)
|
||||||
|
loadConversationHistory(panel, message.offset || 0, message.limit || 10);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "selectConversation":
|
||||||
|
// 选择会话(暂未实现)
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "createNewConversation":
|
||||||
|
// 创建新会话 - 在当前编辑器组中打开新标签页
|
||||||
|
showICHelperPanel(context, panel.viewColumn);
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
**加载会话历史实现**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function loadConversationHistory(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
offset: number = 0,
|
||||||
|
limit: number = 10
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||||
|
|
||||||
|
if (!workspacePath) {
|
||||||
|
// 没有打开的工作区,返回空历史
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "conversationHistory",
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
hasMore: false,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取会话历史列表(支持分页)
|
||||||
|
const result = await historyManager.getConversationHistoryList(
|
||||||
|
workspacePath,
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
);
|
||||||
|
|
||||||
|
// 发送会话历史到前端
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "conversationHistory",
|
||||||
|
items: result.items,
|
||||||
|
total: result.total,
|
||||||
|
hasMore: result.hasMore,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载会话历史失败:", error);
|
||||||
|
// 发生错误时返回空历史
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "conversationHistory",
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
hasMore: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 使用示例
|
||||||
|
|
||||||
|
### 6.1 创建新任务并保存对话
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
|
||||||
|
// 创建新任务
|
||||||
|
const task = await historyManager.createTask(
|
||||||
|
'C:\\Users\\admin\\Documents\\Project',
|
||||||
|
'实现计数器功能'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加用户消息
|
||||||
|
await historyManager.addUserMessage('请帮我生成一个4位计数器');
|
||||||
|
|
||||||
|
// 添加 AI 消息
|
||||||
|
await historyManager.addAiMessage(
|
||||||
|
'好的,我来帮你生成一个4位计数器...',
|
||||||
|
[{ id: '1', toolName: 'generateCode', parameters: {} }]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加工具执行结果
|
||||||
|
await historyManager.addToolExecutionResult(
|
||||||
|
'1',
|
||||||
|
'generateCode',
|
||||||
|
'代码生成成功'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 记录对话元数据
|
||||||
|
await historyManager.recordTurnMeta(
|
||||||
|
1,
|
||||||
|
{ inputTokens: 120, outputTokens: 350, totalTokens: 470 },
|
||||||
|
'gpt-4',
|
||||||
|
2500
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 查询会话历史
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
|
||||||
|
// 获取第一页(前10条)
|
||||||
|
const page1 = await historyManager.getConversationHistoryList(
|
||||||
|
'C:\\Users\\admin\\Documents\\Project',
|
||||||
|
0,
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('总数:', page1.total);
|
||||||
|
console.log('是否还有更多:', page1.hasMore);
|
||||||
|
console.log('会话列表:', page1.items);
|
||||||
|
|
||||||
|
// 获取第二页(第11-20条)
|
||||||
|
const page2 = await historyManager.getConversationHistoryList(
|
||||||
|
'C:\\Users\\admin\\Documents\\Project',
|
||||||
|
10,
|
||||||
|
10
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 切换任务
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
|
||||||
|
// 切换到指定任务
|
||||||
|
const success = await historyManager.switchTask(
|
||||||
|
'C:\\Users\\admin\\Documents\\Project',
|
||||||
|
'task_20231226_a3f9k2'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// 获取当前任务会话
|
||||||
|
const session = await historyManager.getCurrentTaskSession();
|
||||||
|
console.log('任务元数据:', session.meta);
|
||||||
|
console.log('对话历史:', session.messages);
|
||||||
|
console.log('对话元数据:', session.conversationMeta);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 性能优化
|
||||||
|
|
||||||
|
### 7.1 分页加载
|
||||||
|
|
||||||
|
- 前端默认每页加载 10 条记录
|
||||||
|
- 最多显示 100 条历史记录
|
||||||
|
- 滚动到底部时自动加载下一页
|
||||||
|
|
||||||
|
### 7.2 懒加载
|
||||||
|
|
||||||
|
- 只在打开下拉菜单时才加载会话历史
|
||||||
|
- 避免不必要的文件读取操作
|
||||||
|
|
||||||
|
### 7.3 缓存机制
|
||||||
|
|
||||||
|
- 前端缓存已加载的会话列表
|
||||||
|
- 避免重复请求相同数据
|
||||||
|
|
||||||
|
### 7.4 文件格式优化
|
||||||
|
|
||||||
|
- 使用 JSONL 格式存储对话元数据,支持追加写入
|
||||||
|
- 避免频繁读写整个文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 错误处理
|
||||||
|
|
||||||
|
### 8.1 目录不存在
|
||||||
|
|
||||||
|
系统会自动创建不存在的目录:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private async ensureTaskDir(taskDir: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const uri = vscode.Uri.file(taskDir);
|
||||||
|
try {
|
||||||
|
await vscode.workspace.fs.stat(uri);
|
||||||
|
} catch {
|
||||||
|
// 目录不存在,创建它
|
||||||
|
await vscode.workspace.fs.createDirectory(uri);
|
||||||
|
console.log(`创建任务目录: ${taskDir}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("创建任务目录失败:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 文件读取失败
|
||||||
|
|
||||||
|
读取失败时返回默认值:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private async loadConversation(): Promise<ChatMessage[]> {
|
||||||
|
try {
|
||||||
|
const uri = vscode.Uri.file(conversationPath);
|
||||||
|
const content = await vscode.workspace.fs.readFile(uri);
|
||||||
|
const data = Buffer.from(content).toString('utf-8');
|
||||||
|
return JSON.parse(data);
|
||||||
|
} catch (error) {
|
||||||
|
// 文件不存在或读取失败,返回空数组
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 无工作区处理
|
||||||
|
|
||||||
|
没有打开工作区时,自动创建默认任务:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private async ensureCurrentTask(): Promise<void> {
|
||||||
|
if (!this.currentTaskId || !this.currentProjectPath) {
|
||||||
|
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||||
|
if (workspacePath) {
|
||||||
|
await this.createTask(workspacePath, "默认任务");
|
||||||
|
} else {
|
||||||
|
throw new Error("没有打开的工作区,无法创建任务");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 未来扩展
|
||||||
|
|
||||||
|
### 9.1 会话切换功能
|
||||||
|
|
||||||
|
目前 `selectConversation` 功能暂未实现,未来可以支持:
|
||||||
|
- 点击历史会话,加载该会话的完整对话历史
|
||||||
|
- 在新标签页中打开历史会话
|
||||||
|
- 继续历史会话的对话
|
||||||
|
|
||||||
|
### 9.2 会话搜索
|
||||||
|
|
||||||
|
- 支持按关键词搜索会话
|
||||||
|
- 支持按时间范围筛选
|
||||||
|
- 支持按 Token 使用量排序
|
||||||
|
|
||||||
|
### 9.3 会话导出
|
||||||
|
|
||||||
|
- 导出为 Markdown 格式
|
||||||
|
- 导出为 JSON 格式
|
||||||
|
- 导出为 PDF 格式
|
||||||
|
|
||||||
|
### 9.4 会话统计
|
||||||
|
|
||||||
|
- 显示总对话轮次
|
||||||
|
- 显示总 Token 使用量
|
||||||
|
- 显示平均响应时间
|
||||||
|
|
||||||
|
### 9.5 云端同步
|
||||||
|
|
||||||
|
- 支持将会话数据同步到云端
|
||||||
|
- 支持多设备访问
|
||||||
|
- 支持团队协作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 总结
|
||||||
|
|
||||||
|
IC Coder 的会话存储系统采用文件系统存储方案,具有以下优势:
|
||||||
|
|
||||||
|
1. **简单可靠**:无需额外的数据库依赖
|
||||||
|
2. **易于备份**:直接复制文件即可备份
|
||||||
|
3. **跨平台**:支持 Windows、macOS、Linux
|
||||||
|
4. **可扩展**:易于添加新的数据字段
|
||||||
|
5. **高性能**:分页加载,避免一次性加载大量数据
|
||||||
|
|
||||||
|
系统已经实现了核心的会话管理功能,包括任务创建、消息保存、历史查询等,为用户提供了完整的会话历史管理体验。
|
||||||
@ -3,8 +3,9 @@
|
|||||||
"displayName": "IC Coder plugin",
|
"displayName": "IC Coder plugin",
|
||||||
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
|
"description": "Agentic Verilog Coding Platform for Real-World FPGAs",
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
|
"publisher": "ic-coder-team",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.107.0"
|
"vscode": "^1.80.0"
|
||||||
},
|
},
|
||||||
"icon": "media/图案(方底).png",
|
"icon": "media/图案(方底).png",
|
||||||
"categories": [
|
"categories": [
|
||||||
@ -128,9 +129,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mocha": "^10.0.10",
|
"@types/mocha": "^10.0.10",
|
||||||
"@types/node": "22.x",
|
"@types/node": "22.x",
|
||||||
"@types/vscode": "^1.107.0",
|
"@types/vscode": "^1.80.0",
|
||||||
"@vscode/test-cli": "^0.0.12",
|
"@vscode/test-cli": "^0.0.12",
|
||||||
"@vscode/test-electron": "^2.5.2",
|
"@vscode/test-electron": "^2.5.2",
|
||||||
|
"@vscode/vsce": "^3.7.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"ts-loader": "^9.5.4",
|
"ts-loader": "^9.5.4",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
|||||||
1944
pnpm-lock.yaml
generated
1944
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -13,16 +13,16 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
// 注册命令:打开助手面板
|
// 注册命令:打开助手面板
|
||||||
const openPanelCommand = vscode.commands.registerCommand(
|
const openPanelCommand = vscode.commands.registerCommand(
|
||||||
"ic-coder.openPanel",
|
"ic-coder.openPanel",
|
||||||
() => {
|
async () => {
|
||||||
showICHelperPanel(context);
|
await showICHelperPanel(context);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 注册命令:打开聊天(用于侧边栏)
|
// 注册命令:打开聊天(用于侧边栏)
|
||||||
const openChatCommand = vscode.commands.registerCommand(
|
const openChatCommand = vscode.commands.registerCommand(
|
||||||
"ic-coder.openChat",
|
"ic-coder.openChat",
|
||||||
() => {
|
async () => {
|
||||||
showICHelperPanel(context);
|
await showICHelperPanel(context);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -11,11 +11,13 @@ import {
|
|||||||
abortCurrentDialog,
|
abortCurrentDialog,
|
||||||
} from "../utils/messageHandler";
|
} from "../utils/messageHandler";
|
||||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||||
|
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||||
|
import { MessageType } from "../types/chatHistory";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建并显示 IC 助手面板
|
* 创建并显示 IC 助手面板
|
||||||
*/
|
*/
|
||||||
export function showICHelperPanel(
|
export async function showICHelperPanel(
|
||||||
context: vscode.ExtensionContext,
|
context: vscode.ExtensionContext,
|
||||||
viewColumn?: vscode.ViewColumn
|
viewColumn?: vscode.ViewColumn
|
||||||
) {
|
) {
|
||||||
@ -31,6 +33,10 @@ export function showICHelperPanel(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 为面板生成唯一ID
|
||||||
|
const panelId = `panel_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
(panel as any).__uniqueId = panelId;
|
||||||
|
|
||||||
// 设置标签页图标
|
// 设置标签页图标
|
||||||
panel.iconPath = vscode.Uri.joinPath(
|
panel.iconPath = vscode.Uri.joinPath(
|
||||||
context.extensionUri,
|
context.extensionUri,
|
||||||
@ -48,9 +54,29 @@ export function showICHelperPanel(
|
|||||||
|
|
||||||
// 处理消息
|
// 处理消息
|
||||||
panel.webview.onDidReceiveMessage(
|
panel.webview.onDidReceiveMessage(
|
||||||
(message) => {
|
async (message) => {
|
||||||
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
const panelId = (panel as any).__uniqueId;
|
||||||
|
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
case "sendMessage":
|
case "sendMessage":
|
||||||
|
// 仅在用户发送消息时,确保面板有任务上下文
|
||||||
|
// 如果没有,则创建新任务(仅在首次发送消息时)
|
||||||
|
if (!historyManager.getPanelTask(panelId)) {
|
||||||
|
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||||
|
if (workspacePath) {
|
||||||
|
try {
|
||||||
|
const taskMeta = await historyManager.createTask(workspacePath, "新对话");
|
||||||
|
historyManager.setPanelTask(panelId, taskMeta.taskId, workspacePath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("创建任务失败:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换到当前面板的任务上下文
|
||||||
|
historyManager.switchToPanelTask(panelId);
|
||||||
|
|
||||||
handleUserMessage(panel, message.text, context.extensionPath);
|
handleUserMessage(panel, message.text, context.extensionPath);
|
||||||
break;
|
break;
|
||||||
case "readFile":
|
case "readFile":
|
||||||
@ -96,14 +122,14 @@ export function showICHelperPanel(
|
|||||||
showICHelperPanel(context, panel.viewColumn);
|
showICHelperPanel(context, panel.viewColumn);
|
||||||
break;
|
break;
|
||||||
case "loadConversationHistory":
|
case "loadConversationHistory":
|
||||||
// 加载会话历史(暂未实现)
|
// 加载会话历史(支持分页)
|
||||||
panel.webview.postMessage({
|
loadConversationHistory(panel, message.offset || 0, message.limit || 10);
|
||||||
command: "conversationHistory",
|
|
||||||
history: [],
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case "selectConversation":
|
case "selectConversation":
|
||||||
// 选择会话(暂未实现)
|
// 选择会话
|
||||||
|
if (message.conversationId) {
|
||||||
|
selectConversation(panel, message.conversationId, context.extensionPath);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
// 新增:处理用户回答
|
// 新增:处理用户回答
|
||||||
case "submitAnswer":
|
case "submitAnswer":
|
||||||
@ -122,6 +148,17 @@ export function showICHelperPanel(
|
|||||||
undefined,
|
undefined,
|
||||||
context.subscriptions
|
context.subscriptions
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 面板关闭时清理任务映射
|
||||||
|
panel.onDidDispose(
|
||||||
|
() => {
|
||||||
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
const panelId = (panel as any).__uniqueId;
|
||||||
|
historyManager.removePanelTask(panelId);
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
context.subscriptions
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -302,3 +339,226 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
|
|||||||
|
|
||||||
return signals;
|
return signals;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载会话历史(支持分页)
|
||||||
|
*/
|
||||||
|
async function loadConversationHistory(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
offset: number = 0,
|
||||||
|
limit: number = 10
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||||
|
|
||||||
|
if (!workspacePath) {
|
||||||
|
// 没有打开的工作区,返回空历史
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "conversationHistory",
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
hasMore: false,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取会话历史列表(支持分页)
|
||||||
|
const result = await historyManager.getConversationHistoryList(
|
||||||
|
workspacePath,
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
);
|
||||||
|
|
||||||
|
// 发送会话历史到前端
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "conversationHistory",
|
||||||
|
items: result.items,
|
||||||
|
total: result.total,
|
||||||
|
hasMore: result.hasMore,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载会话历史失败:", error);
|
||||||
|
// 发生错误时返回空历史
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "conversationHistory",
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
hasMore: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择并加载指定的会话
|
||||||
|
*/
|
||||||
|
async function selectConversation(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
taskId: string,
|
||||||
|
extensionPath: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||||
|
|
||||||
|
if (!workspacePath) {
|
||||||
|
vscode.window.showErrorMessage("没有打开的工作区");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载任务会话
|
||||||
|
const taskSession = await historyManager.loadTaskSession(workspacePath, taskId);
|
||||||
|
|
||||||
|
if (!taskSession) {
|
||||||
|
vscode.window.showErrorMessage(`加载任务 ${taskId} 失败: 任务不存在或数据损坏`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换到该任务
|
||||||
|
const switched = await historyManager.switchTask(workspacePath, taskId);
|
||||||
|
if (!switched) {
|
||||||
|
vscode.window.showErrorMessage(`切换到任务 ${taskId} 失败`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新面板的任务映射,确保后续对话保存到正确的任务中
|
||||||
|
const panelId = (panel as any).__uniqueId;
|
||||||
|
historyManager.setPanelTask(panelId, taskId, workspacePath);
|
||||||
|
|
||||||
|
// 清空当前聊天界面
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "clearChat"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将会话历史消息转换为 segments 格式并发送到前端显示
|
||||||
|
const segments: any[] = [];
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < taskSession.messages.length) {
|
||||||
|
const message = taskSession.messages[i];
|
||||||
|
|
||||||
|
if (message.type === MessageType.USER) {
|
||||||
|
// 用户消息 - 如果有累积的 segments,先发送
|
||||||
|
if (segments.length > 0) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "receiveSegments",
|
||||||
|
segments: [...segments]
|
||||||
|
});
|
||||||
|
segments.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送用户消息
|
||||||
|
const textContent = message.contents?.find(c => c.type === 'TEXT');
|
||||||
|
if (textContent && 'text' in textContent) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "addUserMessage",
|
||||||
|
text: textContent.text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
} else if (message.type === MessageType.AI) {
|
||||||
|
// AI消息 - 如果有 segments,直接使用
|
||||||
|
if (message.segments && message.segments.length > 0) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "receiveSegments",
|
||||||
|
segments: message.segments
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
// 旧格式:需要转换为 segments
|
||||||
|
// 收集连续的 AI 消息、工具调用和工具结果
|
||||||
|
if (message.text) {
|
||||||
|
segments.push({
|
||||||
|
type: 'text',
|
||||||
|
content: message.text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有工具调用
|
||||||
|
if (message.toolExecutionRequests && message.toolExecutionRequests.length > 0) {
|
||||||
|
for (const toolReq of message.toolExecutionRequests) {
|
||||||
|
// 查找对应的工具执行结果
|
||||||
|
let toolResult = '';
|
||||||
|
if (i + 1 < taskSession.messages.length) {
|
||||||
|
const nextMsg = taskSession.messages[i + 1];
|
||||||
|
if (nextMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
|
||||||
|
nextMsg.id === toolReq.id) {
|
||||||
|
toolResult = nextMsg.text;
|
||||||
|
i++; // 跳过工具结果消息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.push({
|
||||||
|
type: 'tool',
|
||||||
|
toolName: toolReq.name,
|
||||||
|
askId: toolReq.id,
|
||||||
|
toolResult: toolResult
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
|
||||||
|
// 继续收集后续的 AI 消息,直到遇到用户消息或有 segments 的 AI 消息
|
||||||
|
while (i < taskSession.messages.length) {
|
||||||
|
const nextMsg = taskSession.messages[i];
|
||||||
|
if (nextMsg.type === MessageType.USER) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (nextMsg.type === MessageType.AI) {
|
||||||
|
if (nextMsg.segments && nextMsg.segments.length > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (nextMsg.text) {
|
||||||
|
segments.push({
|
||||||
|
type: 'text',
|
||||||
|
content: nextMsg.text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (nextMsg.toolExecutionRequests && nextMsg.toolExecutionRequests.length > 0) {
|
||||||
|
for (const toolReq of nextMsg.toolExecutionRequests) {
|
||||||
|
let toolResult = '';
|
||||||
|
if (i + 1 < taskSession.messages.length) {
|
||||||
|
const resultMsg = taskSession.messages[i + 1];
|
||||||
|
if (resultMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
|
||||||
|
resultMsg.id === toolReq.id) {
|
||||||
|
toolResult = resultMsg.text;
|
||||||
|
i++; // 跳过工具结果消息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
segments.push({
|
||||||
|
type: 'tool',
|
||||||
|
toolName: toolReq.name,
|
||||||
|
askId: toolReq.id,
|
||||||
|
toolResult: toolResult
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
} else if (nextMsg.type === MessageType.TOOL_EXECUTION_RESULT) {
|
||||||
|
// 独立的工具结果(没有被上面处理的)
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送剩余的 segments
|
||||||
|
if (segments.length > 0) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "receiveSegments",
|
||||||
|
segments: segments
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
vscode.window.showInformationMessage(`已加载会话: ${taskSession.meta.taskName}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("选择会话失败:", error);
|
||||||
|
vscode.window.showErrorMessage(`加载会话失败: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export interface AiMessage extends BaseMessage {
|
|||||||
text?: string;
|
text?: string;
|
||||||
toolExecutionRequests?: ToolExecutionRequest[];
|
toolExecutionRequests?: ToolExecutionRequest[];
|
||||||
thinking?: string;
|
thinking?: string;
|
||||||
|
segments?: any[]; // 保存完整的 segments 信息用于还原显示
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -8,7 +8,8 @@ import {
|
|||||||
MessageType,
|
MessageType,
|
||||||
UserMessage,
|
UserMessage,
|
||||||
AiMessage,
|
AiMessage,
|
||||||
SystemMessage
|
SystemMessage,
|
||||||
|
ToolExecutionResultMessage
|
||||||
} from '../types/chatHistory';
|
} from '../types/chatHistory';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,6 +21,8 @@ export class ChatHistoryManager {
|
|||||||
private baseDir: string; // ~/.iccoder
|
private baseDir: string; // ~/.iccoder
|
||||||
private currentTaskId: string | null = null;
|
private currentTaskId: string | null = null;
|
||||||
private currentProjectPath: string | null = null;
|
private currentProjectPath: string | null = null;
|
||||||
|
// 存储每个面板的任务信息(taskId 和 projectPath)
|
||||||
|
private panelTaskMap: Map<string, { taskId: string; projectPath: string }> = new Map();
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
// 设置存储路径: ~/.iccoder
|
// 设置存储路径: ~/.iccoder
|
||||||
@ -33,12 +36,13 @@ export class ChatHistoryManager {
|
|||||||
* 规则:
|
* 规则:
|
||||||
* - 替换 \ 和 / 为 --
|
* - 替换 \ 和 / 为 --
|
||||||
* - 替换 : 为空
|
* - 替换 : 为空
|
||||||
* 例如:C:\Users\admin\Documents\Project -> C--Users-admin-Documents-Project
|
* 例如:C:\Users\admin\Documents\Project -> C--Users--admin--Documents--Project
|
||||||
*/
|
*/
|
||||||
private encodeProjectPath(projectPath: string): string {
|
private encodeProjectPath(projectPath: string): string {
|
||||||
return projectPath
|
return projectPath
|
||||||
.replace(/:/g, '') // 移除冒号
|
.replace(/\\/g, '--') // 替换反斜杠为 --
|
||||||
.replace(/[/\\]/g, '--'); // 替换斜杠为 --
|
.replace(/\//g, '--') // 替换正斜杠为 --
|
||||||
|
.replace(/:/g, ''); // 移除冒号
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -107,6 +111,43 @@ export class ChatHistoryManager {
|
|||||||
return ChatHistoryManager.instance;
|
return ChatHistoryManager.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为面板设置任务ID
|
||||||
|
*/
|
||||||
|
public setPanelTask(panelId: string, taskId: string, projectPath: string): void {
|
||||||
|
this.panelTaskMap.set(panelId, { taskId, projectPath });
|
||||||
|
this.currentTaskId = taskId;
|
||||||
|
this.currentProjectPath = projectPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取面板的任务ID
|
||||||
|
*/
|
||||||
|
public getPanelTask(panelId: string): string | null {
|
||||||
|
const taskInfo = this.panelTaskMap.get(panelId);
|
||||||
|
return taskInfo ? taskInfo.taskId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换到指定面板的任务上下文
|
||||||
|
*/
|
||||||
|
public switchToPanelTask(panelId: string): boolean {
|
||||||
|
const taskInfo = this.panelTaskMap.get(panelId);
|
||||||
|
if (taskInfo) {
|
||||||
|
this.currentTaskId = taskInfo.taskId;
|
||||||
|
this.currentProjectPath = taskInfo.projectPath;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除面板的任务映射
|
||||||
|
*/
|
||||||
|
public removePanelTask(panelId: string): void {
|
||||||
|
this.panelTaskMap.delete(panelId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建新任务
|
* 创建新任务
|
||||||
*/
|
*/
|
||||||
@ -264,17 +305,11 @@ export class ChatHistoryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确保有当前任务,如果没有则自动创建
|
* 确保有当前任务,如果没有则抛出错误
|
||||||
*/
|
*/
|
||||||
private async ensureCurrentTask(): Promise<void> {
|
private async ensureCurrentTask(): Promise<void> {
|
||||||
if (!this.currentTaskId || !this.currentProjectPath) {
|
if (!this.currentTaskId || !this.currentProjectPath) {
|
||||||
// 获取当前工作区路径
|
throw new Error("没有当前任务上下文,请确保已正确初始化面板任务");
|
||||||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
||||||
if (workspacePath) {
|
|
||||||
await this.createTask(workspacePath, "默认任务");
|
|
||||||
} else {
|
|
||||||
throw new Error("没有打开的工作区,无法创建任务");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,14 +335,15 @@ export class ChatHistoryManager {
|
|||||||
/**
|
/**
|
||||||
* 添加AI消息
|
* 添加AI消息
|
||||||
*/
|
*/
|
||||||
public async addAiMessage(text: string, toolRequests?: any[]): Promise<void> {
|
public async addAiMessage(text: string, toolRequests?: any[], segments?: any[]): Promise<void> {
|
||||||
await this.ensureCurrentTask();
|
await this.ensureCurrentTask();
|
||||||
const messages = await this.loadConversation();
|
const messages = await this.loadConversation();
|
||||||
|
|
||||||
const aiMessage: AiMessage = {
|
const aiMessage: AiMessage = {
|
||||||
type: MessageType.AI,
|
type: MessageType.AI,
|
||||||
text,
|
text,
|
||||||
toolExecutionRequests: toolRequests
|
toolExecutionRequests: toolRequests,
|
||||||
|
segments // 保存完整的 segments 信息
|
||||||
};
|
};
|
||||||
|
|
||||||
messages.push(aiMessage);
|
messages.push(aiMessage);
|
||||||
@ -333,6 +369,24 @@ export class ChatHistoryManager {
|
|||||||
await this.saveConversation(messages);
|
await this.saveConversation(messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加工具执行结果消息
|
||||||
|
*/
|
||||||
|
public async addToolExecutionResult(id: string, toolName: string, result: string): Promise<void> {
|
||||||
|
await this.ensureCurrentTask();
|
||||||
|
const messages = await this.loadConversation();
|
||||||
|
|
||||||
|
const toolResultMessage: ToolExecutionResultMessage = {
|
||||||
|
type: MessageType.TOOL_EXECUTION_RESULT,
|
||||||
|
id,
|
||||||
|
toolName,
|
||||||
|
text: result
|
||||||
|
};
|
||||||
|
|
||||||
|
messages.push(toolResultMessage);
|
||||||
|
await this.saveConversation(messages);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录对话轮次元数据
|
* 记录对话轮次元数据
|
||||||
*/
|
*/
|
||||||
@ -450,6 +504,20 @@ export class ChatHistoryManager {
|
|||||||
tasks.push(JSON.parse(data));
|
tasks.push(JSON.parse(data));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`加载任务 ${taskId} 失败:`, error);
|
console.error(`加载任务 ${taskId} 失败:`, error);
|
||||||
|
// 跳过无效的任务目录
|
||||||
|
// 尝试清理空目录
|
||||||
|
try {
|
||||||
|
const taskDirUri = vscode.Uri.file(path.join(projectDir, taskId));
|
||||||
|
const taskDirEntries = await vscode.workspace.fs.readDirectory(taskDirUri);
|
||||||
|
if (taskDirEntries.length === 0) {
|
||||||
|
// 目录为空,删除它
|
||||||
|
await vscode.workspace.fs.delete(taskDirUri, { recursive: false });
|
||||||
|
console.log(`已清理空任务目录: ${taskId}`);
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
// 清理失败,忽略错误
|
||||||
|
console.warn(`清理任务目录 ${taskId} 失败:`, cleanupError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -494,4 +562,132 @@ export class ChatHistoryManager {
|
|||||||
public getBaseDir(): string {
|
public getBaseDir(): string {
|
||||||
return this.baseDir;
|
return this.baseDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载指定任务的会话内容
|
||||||
|
* @param projectPath 项目路径
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @returns 任务会话内容,如果任务不存在则返回null
|
||||||
|
*/
|
||||||
|
public async loadTaskSession(projectPath: string, taskId: string): Promise<TaskSession | null> {
|
||||||
|
const taskDir = this.getTaskDir(projectPath, taskId);
|
||||||
|
const metaPath = path.join(taskDir, 'meta.json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查任务是否存在
|
||||||
|
const metaUri = vscode.Uri.file(metaPath);
|
||||||
|
const metaContent = await vscode.workspace.fs.readFile(metaUri);
|
||||||
|
const meta: TaskMeta = JSON.parse(Buffer.from(metaContent).toString('utf-8'));
|
||||||
|
|
||||||
|
// 读取会话内容
|
||||||
|
const conversationPath = path.join(taskDir, 'conversation.json');
|
||||||
|
let messages: ChatMessage[] = [];
|
||||||
|
try {
|
||||||
|
const conversationUri = vscode.Uri.file(conversationPath);
|
||||||
|
const conversationContent = await vscode.workspace.fs.readFile(conversationUri);
|
||||||
|
messages = JSON.parse(Buffer.from(conversationContent).toString('utf-8'));
|
||||||
|
} catch {
|
||||||
|
// 会话文件不存在,使用空数组
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取会话元数据
|
||||||
|
const conversationMetaPath = path.join(taskDir, 'conversation_meta.jsonl');
|
||||||
|
let conversationMeta: ConversationMeta[] = [];
|
||||||
|
try {
|
||||||
|
const metaUri = vscode.Uri.file(conversationMetaPath);
|
||||||
|
const content = await vscode.workspace.fs.readFile(metaUri);
|
||||||
|
const data = Buffer.from(content).toString('utf-8');
|
||||||
|
conversationMeta = data
|
||||||
|
.split('\n')
|
||||||
|
.filter(line => line.trim())
|
||||||
|
.map(line => JSON.parse(line));
|
||||||
|
} catch {
|
||||||
|
// 元数据文件不存在,使用空数组
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
meta,
|
||||||
|
messages,
|
||||||
|
conversationMeta
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`加载任务 ${taskId} 的会话失败:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话历史列表(支持分页)
|
||||||
|
* 返回格式:{ id: taskId, title: 第一句用户消息, timestamp: 创建时间 }
|
||||||
|
* @param projectPath 项目路径
|
||||||
|
* @param offset 偏移量(从第几条开始,默认0)
|
||||||
|
* @param limit 每页数量(默认10条)
|
||||||
|
* @returns { items: 历史列表, total: 总数, hasMore: 是否还有更多 }
|
||||||
|
*/
|
||||||
|
public async getConversationHistoryList(
|
||||||
|
projectPath: string,
|
||||||
|
offset: number = 0,
|
||||||
|
limit: number = 10
|
||||||
|
): Promise<{
|
||||||
|
items: Array<{ id: string; title: string; timestamp: string }>;
|
||||||
|
total: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
}> {
|
||||||
|
const tasks = await this.listProjectTasks(projectPath);
|
||||||
|
const total = tasks.length;
|
||||||
|
const historyList: Array<{ id: string; title: string; timestamp: string }> = [];
|
||||||
|
|
||||||
|
// 计算分页范围
|
||||||
|
const start = offset;
|
||||||
|
const end = Math.min(offset + limit, total);
|
||||||
|
const limitedTasks = tasks.slice(start, end);
|
||||||
|
|
||||||
|
for (const task of limitedTasks) {
|
||||||
|
// 读取该任务的 conversation.json 获取第一句用户消息
|
||||||
|
const taskDir = this.getTaskDir(task.projectPath, task.taskId);
|
||||||
|
const conversationPath = path.join(taskDir, 'conversation.json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uri = vscode.Uri.file(conversationPath);
|
||||||
|
const content = await vscode.workspace.fs.readFile(uri);
|
||||||
|
const data = Buffer.from(content).toString('utf-8');
|
||||||
|
const messages: ChatMessage[] = JSON.parse(data);
|
||||||
|
|
||||||
|
// 找到第一条用户消息
|
||||||
|
const firstUserMessage = messages.find(msg => msg.type === MessageType.USER) as UserMessage | undefined;
|
||||||
|
|
||||||
|
let title = '未命名会话';
|
||||||
|
if (firstUserMessage && firstUserMessage.contents && firstUserMessage.contents.length > 0) {
|
||||||
|
const textContent = firstUserMessage.contents.find(c => c.type === 'TEXT');
|
||||||
|
if (textContent && 'text' in textContent) {
|
||||||
|
// 截取前50个字符作为标题
|
||||||
|
title = textContent.text.length > 50
|
||||||
|
? textContent.text.substring(0, 50) + '...'
|
||||||
|
: textContent.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
historyList.push({
|
||||||
|
id: task.taskId,
|
||||||
|
title,
|
||||||
|
timestamp: task.createdAt
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`读取任务 ${task.taskId} 的会话历史失败:`, error);
|
||||||
|
// 如果读取失败,使用任务名称作为标题
|
||||||
|
historyList.push({
|
||||||
|
id: task.taskId,
|
||||||
|
title: task.taskName || '未命名会话',
|
||||||
|
timestamp: task.createdAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回分页结果
|
||||||
|
return {
|
||||||
|
items: historyList,
|
||||||
|
total,
|
||||||
|
hasMore: end < total
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -170,6 +170,20 @@ async function handleUserMessageWithBackend(
|
|||||||
});
|
});
|
||||||
console.log('[MessageHandler] postMessage 返回值:', result);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
71
src/views/contextButton.ts
Normal file
71
src/views/contextButton.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* 添加上下文按钮组件
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取添加上下文按钮的 HTML 内容
|
||||||
|
*/
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取添加上下文按钮的样式
|
||||||
|
*/
|
||||||
|
export function getContextButtonStyles(): string {
|
||||||
|
return `
|
||||||
|
/* 添加上下文按钮样式 */
|
||||||
|
.add-context-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(128, 128, 128, 0.2);
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-context-button:hover {
|
||||||
|
background: rgba(128, 128, 128, 0.3);
|
||||||
|
border-color: var(--vscode-focusBorder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-context-button svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-context-label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取添加上下文按钮的脚本
|
||||||
|
*/
|
||||||
|
export function getContextButtonScript(): string {
|
||||||
|
return `
|
||||||
|
// 添加上下文处理函数
|
||||||
|
function handleAddContext() {
|
||||||
|
// 发送添加上下文请求到扩展
|
||||||
|
vscode.postMessage({ command: 'addContext' });
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -111,6 +111,10 @@ export function getConversationHistoryBarStyles(): string {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
border-bottom: 1px solid var(--vscode-panel-border);
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-item:last-child {
|
.history-item:last-child {
|
||||||
@ -124,15 +128,17 @@ export function getConversationHistoryBarStyles(): string {
|
|||||||
.history-item-title {
|
.history-item-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 4px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-item-time {
|
.history-item-time {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-empty {
|
.history-empty {
|
||||||
@ -142,6 +148,14 @@ export function getConversationHistoryBarStyles(): string {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-load-more {
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
font-size: 12px;
|
||||||
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
|
||||||
.new-conversation-button {
|
.new-conversation-button {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
@ -199,6 +213,12 @@ export function getConversationHistoryBarScript(): string {
|
|||||||
// 会话历史相关变量
|
// 会话历史相关变量
|
||||||
let conversationHistory = [];
|
let conversationHistory = [];
|
||||||
let currentConversationId = null;
|
let currentConversationId = null;
|
||||||
|
let currentOffset = 0;
|
||||||
|
let totalHistory = 0;
|
||||||
|
let hasMoreHistory = false;
|
||||||
|
let isLoadingHistory = false;
|
||||||
|
const HISTORY_PAGE_SIZE = 10;
|
||||||
|
const MAX_HISTORY_ITEMS = 100;
|
||||||
|
|
||||||
// 切换历史记录下拉菜单
|
// 切换历史记录下拉菜单
|
||||||
function toggleHistoryDropdown() {
|
function toggleHistoryDropdown() {
|
||||||
@ -211,33 +231,90 @@ export function getConversationHistoryBarScript(): string {
|
|||||||
} else {
|
} else {
|
||||||
menu.classList.add('active');
|
menu.classList.add('active');
|
||||||
button.classList.add('active');
|
button.classList.add('active');
|
||||||
// 加载会话历史
|
// 重置并加载会话历史
|
||||||
loadConversationHistory();
|
resetAndLoadHistory();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载会话历史
|
// 重置并加载会话历史
|
||||||
function loadConversationHistory() {
|
function resetAndLoadHistory() {
|
||||||
vscode.postMessage({ command: 'loadConversationHistory' });
|
conversationHistory = [];
|
||||||
|
currentOffset = 0;
|
||||||
|
totalHistory = 0;
|
||||||
|
hasMoreHistory = false;
|
||||||
|
const historyList = document.getElementById('historyList');
|
||||||
|
if (historyList) {
|
||||||
|
historyList.innerHTML = '<div class="history-empty">加载中...</div>';
|
||||||
|
}
|
||||||
|
loadMoreHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染会话历史列表
|
// 加载更多会话历史
|
||||||
function renderConversationHistory(history) {
|
function loadMoreHistory() {
|
||||||
conversationHistory = history;
|
if (isLoadingHistory || (currentOffset > 0 && !hasMoreHistory)) {
|
||||||
const historyList = document.getElementById('historyList');
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!history || history.length === 0) {
|
// 检查是否已达到最大数量
|
||||||
|
if (currentOffset >= MAX_HISTORY_ITEMS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingHistory = true;
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'loadConversationHistory',
|
||||||
|
offset: currentOffset,
|
||||||
|
limit: HISTORY_PAGE_SIZE
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染会话历史列表(支持追加)
|
||||||
|
function renderConversationHistory(data) {
|
||||||
|
isLoadingHistory = false;
|
||||||
|
|
||||||
|
if (!data || !data.items) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加新数据
|
||||||
|
conversationHistory = conversationHistory.concat(data.items);
|
||||||
|
totalHistory = data.total;
|
||||||
|
hasMoreHistory = data.hasMore;
|
||||||
|
currentOffset += data.items.length;
|
||||||
|
|
||||||
|
const historyList = document.getElementById('historyList');
|
||||||
|
if (!historyList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有任何历史记录
|
||||||
|
if (conversationHistory.length === 0) {
|
||||||
historyList.innerHTML = '<div class="history-empty">暂无会话历史</div>';
|
historyList.innerHTML = '<div class="history-empty">暂无会话历史</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
historyList.innerHTML = history.map(item => \`
|
// 渲染所有历史记录
|
||||||
<div class="history-item"
|
historyList.innerHTML = conversationHistory.map(item => \`
|
||||||
onclick="selectConversation('\${item.id}')">
|
<div class="history-item" onclick="selectConversation('\${item.id}')">
|
||||||
<div class="history-item-title">\${item.title || '未命名会话'}</div>
|
<div class="history-item-title">\${item.title || '未命名会话'}</div>
|
||||||
<div class="history-item-time">\${formatTime(item.timestamp)}</div>
|
<div class="history-item-time">\${formatTime(item.timestamp)}</div>
|
||||||
</div>
|
</div>
|
||||||
\`).join('');
|
\`).join('');
|
||||||
|
|
||||||
|
// 如果还有更多数据,添加"加载更多"提示
|
||||||
|
if (hasMoreHistory && currentOffset < MAX_HISTORY_ITEMS) {
|
||||||
|
historyList.innerHTML += \`
|
||||||
|
<div class="history-load-more" id="loadMoreIndicator">
|
||||||
|
<span>滚动加载更多...</span>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
} else if (currentOffset >= MAX_HISTORY_ITEMS && hasMoreHistory) {
|
||||||
|
historyList.innerHTML += \`
|
||||||
|
<div class="history-load-more">
|
||||||
|
<span>已显示最近 \${MAX_HISTORY_ITEMS} 条记录</span>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择会话
|
// 选择会话
|
||||||
@ -291,6 +368,22 @@ export function getConversationHistoryBarScript(): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听下拉菜单滚动事件
|
||||||
|
const historyDropdownMenu = document.getElementById('historyDropdownMenu');
|
||||||
|
if (historyDropdownMenu) {
|
||||||
|
historyDropdownMenu.addEventListener('scroll', () => {
|
||||||
|
const menu = historyDropdownMenu;
|
||||||
|
const scrollTop = menu.scrollTop;
|
||||||
|
const scrollHeight = menu.scrollHeight;
|
||||||
|
const clientHeight = menu.clientHeight;
|
||||||
|
|
||||||
|
// 当滚动到距离底部 50px 时,加载更多
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 50) {
|
||||||
|
loadMoreHistory();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 点击外部关闭下拉菜单
|
// 点击外部关闭下拉菜单
|
||||||
document.addEventListener('click', (event) => {
|
document.addEventListener('click', (event) => {
|
||||||
const container = document.querySelector('.history-dropdown-container');
|
const container = document.querySelector('.history-dropdown-container');
|
||||||
|
|||||||
@ -1,4 +1,14 @@
|
|||||||
import { getWaveformPreviewContent } from "./waveformPreviewContent";
|
import { getWaveformPreviewContent } from "./waveformPreviewContent";
|
||||||
|
import {
|
||||||
|
getModelSelectorContent,
|
||||||
|
getModelSelectorStyles,
|
||||||
|
getModelSelectorScript
|
||||||
|
} from "./modelSelector";
|
||||||
|
import {
|
||||||
|
getContextButtonContent,
|
||||||
|
getContextButtonStyles,
|
||||||
|
getContextButtonScript
|
||||||
|
} from "./contextButton";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取输入区域的 HTML 内容
|
* 获取输入区域的 HTML 内容
|
||||||
@ -8,8 +18,11 @@ export function getInputAreaContent(): string {
|
|||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<!-- Plan 开关 -->
|
<!-- 顶部工具栏 -->
|
||||||
<div class="plan-toggle-container">
|
<div class="input-top-toolbar">
|
||||||
|
${getContextButtonContent()}
|
||||||
|
|
||||||
|
<!-- Plan 开关 -->
|
||||||
<div class="tooltip">
|
<div class="tooltip">
|
||||||
<label class="plan-toggle">
|
<label class="plan-toggle">
|
||||||
<input type="checkbox" id="planToggle" onchange="handlePlanToggle()">
|
<input type="checkbox" id="planToggle" onchange="handlePlanToggle()">
|
||||||
@ -26,14 +39,25 @@ export function getInputAreaContent(): string {
|
|||||||
></textarea>
|
></textarea>
|
||||||
<div class="input-bottom-row">
|
<div class="input-bottom-row">
|
||||||
<div class="mode-selector">
|
<div class="mode-selector">
|
||||||
|
<!-- 模式选择 -->
|
||||||
<div class="tooltip">
|
<div class="tooltip">
|
||||||
<select id="modeSelect">
|
<div class="custom-select" id="customSelect">
|
||||||
<option value="agent" selected>Agent</option>
|
<div class="select-trigger" onclick="toggleModeDropdown()">
|
||||||
<option value="ask">Ask</option>
|
<span class="select-value" id="selectValue">Agent</span>
|
||||||
<option value="auto">Auto</option>
|
<svg class="select-arrow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
</select>
|
<path d="M507.8 727.728a30.016 30.016 0 0 1-21.288-8.824L231.104 463.496a30.088 30.088 0 0 1 0-42.568 30.088 30.088 0 0 1 42.568 0l234.128 234.128 234.16-234.128a30.088 30.088 0 0 1 42.568 0 30.088 30.088 0 0 1 0 42.568L529.08 718.904a30 30 0 0 1-21.28 8.824z" fill="#8a8a8a"/>
|
||||||
<span class="tooltiptext">切换模型</span>
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="select-dropdown" id="selectDropdown">
|
||||||
|
<div class="select-option" data-value="agent" onclick="selectMode('agent', 'Agent')">Agent</div>
|
||||||
|
<div class="select-option" data-value="ask" onclick="selectMode('ask', 'Ask')">Ask</div>
|
||||||
|
<div class="select-option" data-value="auto" onclick="selectMode('auto', 'Auto')">Auto</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="tooltiptext">切换模式</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${getModelSelectorContent()}
|
||||||
</div>
|
</div>
|
||||||
<div class="input-actions">
|
<div class="input-actions">
|
||||||
<!-- 上下文显示 -->
|
<!-- 上下文显示 -->
|
||||||
@ -94,6 +118,8 @@ export function getInputAreaContent(): string {
|
|||||||
*/
|
*/
|
||||||
export function getInputAreaStyles(): string {
|
export function getInputAreaStyles(): string {
|
||||||
return `
|
return `
|
||||||
|
${getModelSelectorStyles()}
|
||||||
|
${getContextButtonStyles()}
|
||||||
.input-area {
|
.input-area {
|
||||||
border-top: 1px solid var(--vscode-panel-border);
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
@ -123,11 +149,13 @@ export function getInputAreaStyles(): string {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
/* Plan 开关样式 */
|
/* 顶部工具栏样式 */
|
||||||
.plan-toggle-container {
|
.input-top-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
margin-bottom: -4px;
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
.plan-toggle {
|
.plan-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -182,26 +210,78 @@ export function getInputAreaStyles(): string {
|
|||||||
.mode-selector {
|
.mode-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
/* 自定义下拉框样式(用于模式选择) */
|
||||||
|
.custom-select {
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.select-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
.select-trigger:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
.select-value {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.select-arrow {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
.custom-select.active .select-arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
.select-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 2px);
|
||||||
|
left: 0;
|
||||||
|
min-width: 100%;
|
||||||
|
background: var(--vscode-dropdown-background);
|
||||||
|
border: 1px solid var(--vscode-dropdown-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 1100;
|
||||||
|
display: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.custom-select.active .select-dropdown {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* 模式选择器的选项样式 */
|
||||||
|
#selectDropdown .select-option {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
#selectDropdown .select-option:hover {
|
||||||
|
background: rgba(128, 128, 128, 0.3);
|
||||||
|
}
|
||||||
|
#selectDropdown .select-option.selected {
|
||||||
|
background: rgba(128, 128, 128, 0.5);
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
.input-actions {
|
.input-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
.mode-selector select {
|
|
||||||
padding: 2px 4px;
|
|
||||||
background: var(--vscode-input-background);
|
|
||||||
color: var(--vscode-foreground);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
outline: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.mode-selector select:hover {
|
|
||||||
background: var(--vscode-list-hoverBackground);
|
|
||||||
}
|
|
||||||
/* Tooltip 样式 */
|
/* Tooltip 样式 */
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -449,6 +529,9 @@ export function getInputAreaStyles(): string {
|
|||||||
*/
|
*/
|
||||||
export function getInputAreaScript(): string {
|
export function getInputAreaScript(): string {
|
||||||
return `
|
return `
|
||||||
|
${getModelSelectorScript()}
|
||||||
|
${getContextButtonScript()}
|
||||||
|
|
||||||
// 自动调整 textarea 高度
|
// 自动调整 textarea 高度
|
||||||
function autoResizeTextarea() {
|
function autoResizeTextarea() {
|
||||||
if (messageInput) {
|
if (messageInput) {
|
||||||
@ -468,15 +551,65 @@ export function getInputAreaScript(): string {
|
|||||||
messageInput.focus();
|
messageInput.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自定义下拉框相关变量
|
||||||
|
let currentMode = 'agent';
|
||||||
|
|
||||||
|
// 切换模式下拉框显示/隐藏
|
||||||
|
function toggleModeDropdown() {
|
||||||
|
const customSelect = document.getElementById('customSelect');
|
||||||
|
const modelSelect = document.getElementById('modelSelect');
|
||||||
|
if (customSelect) {
|
||||||
|
customSelect.classList.toggle('active');
|
||||||
|
// 关闭模型下拉框
|
||||||
|
if (modelSelect) {
|
||||||
|
modelSelect.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择模式
|
||||||
|
function selectMode(value, label) {
|
||||||
|
currentMode = value;
|
||||||
|
const selectValue = document.getElementById('selectValue');
|
||||||
|
if (selectValue) {
|
||||||
|
selectValue.textContent = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新选中状态
|
||||||
|
const options = document.querySelectorAll('#selectDropdown .select-option');
|
||||||
|
options.forEach(option => {
|
||||||
|
if (option.getAttribute('data-value') === value) {
|
||||||
|
option.classList.add('selected');
|
||||||
|
} else {
|
||||||
|
option.classList.remove('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 关闭下拉框
|
||||||
|
const customSelect = document.getElementById('customSelect');
|
||||||
|
if (customSelect) {
|
||||||
|
customSelect.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击外部关闭下拉框
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
const customSelect = document.getElementById('customSelect');
|
||||||
|
|
||||||
|
if (customSelect && !customSelect.contains(event.target)) {
|
||||||
|
customSelect.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function sendMessage() {
|
function sendMessage() {
|
||||||
const text = messageInput.value.trim();
|
const text = messageInput.value.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
const modeSelect = document.getElementById('modeSelect');
|
const mode = currentMode;
|
||||||
const mode = modeSelect ? modeSelect.value : 'agent';
|
const model = getCurrentModel(); // 从模型选择器组件获取当前模型
|
||||||
|
|
||||||
addMessage(text, 'user');
|
addMessage(text, 'user');
|
||||||
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode });
|
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model });
|
||||||
messageInput.value = '';
|
messageInput.value = '';
|
||||||
autoResizeTextarea(); // 重置输入框高度
|
autoResizeTextarea(); // 重置输入框高度
|
||||||
messageInput.focus();
|
messageInput.focus();
|
||||||
|
|||||||
@ -556,6 +556,19 @@ export function getMessageAreaScript(): string {
|
|||||||
return toolNameMap[toolName] || toolName;
|
return toolNameMap[toolName] || toolName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查用户是否在底部附近(允许50px的误差)
|
||||||
|
function isUserNearBottom() {
|
||||||
|
const threshold = 50;
|
||||||
|
return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能滚动:只有用户在底部附近时才自动滚动
|
||||||
|
function smartScrollToBottom() {
|
||||||
|
if (isUserNearBottom()) {
|
||||||
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 添加消息
|
// 添加消息
|
||||||
function addMessage(text, sender) {
|
function addMessage(text, sender) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
@ -602,7 +615,7 @@ export function getMessageAreaScript(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
messagesEl.appendChild(div);
|
messagesEl.appendChild(div);
|
||||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
smartScrollToBottom();
|
||||||
|
|
||||||
// 添加消息后检查 header 显示状态
|
// 添加消息后检查 header 显示状态
|
||||||
checkHeaderVisibility();
|
checkHeaderVisibility();
|
||||||
@ -672,8 +685,8 @@ export function getMessageAreaScript(): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滚动到底部
|
// 智能滚动到底部
|
||||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
smartScrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 完成流式消息
|
// 完成流式消息
|
||||||
@ -699,7 +712,7 @@ export function getMessageAreaScript(): string {
|
|||||||
currentStreamingMessage = null;
|
currentStreamingMessage = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
smartScrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示加载指示器
|
// 显示加载指示器
|
||||||
@ -715,7 +728,7 @@ export function getMessageAreaScript(): string {
|
|||||||
<span class="loading-text">\${text}</span>
|
<span class="loading-text">\${text}</span>
|
||||||
\`;
|
\`;
|
||||||
messagesEl.appendChild(loadingIndicator);
|
messagesEl.appendChild(loadingIndicator);
|
||||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
smartScrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 隐藏加载指示器
|
// 隐藏加载指示器
|
||||||
@ -930,8 +943,8 @@ export function getMessageAreaScript(): string {
|
|||||||
currentSegmentedMessage = null;
|
currentSegmentedMessage = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滚动到底部
|
// 智能滚动到底部
|
||||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
smartScrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染分段消息(兼容旧代码)
|
// 渲染分段消息(兼容旧代码)
|
||||||
@ -1067,7 +1080,7 @@ export function getMessageAreaScript(): string {
|
|||||||
container.appendChild(actionsDiv);
|
container.appendChild(actionsDiv);
|
||||||
|
|
||||||
messagesEl.appendChild(container);
|
messagesEl.appendChild(container);
|
||||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
smartScrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化文本(支持 Markdown)
|
// 格式化文本(支持 Markdown)
|
||||||
@ -1138,7 +1151,7 @@ export function getMessageAreaScript(): string {
|
|||||||
\${detail ? \`<div class="tool-detail">\${detail}</div>\` : ''}
|
\${detail ? \`<div class="tool-detail">\${detail}</div>\` : ''}
|
||||||
\`;
|
\`;
|
||||||
messagesEl.appendChild(div);
|
messagesEl.appendChild(div);
|
||||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
smartScrollToBottom();
|
||||||
|
|
||||||
// 添加消息后检查 header 显示状态
|
// 添加消息后检查 header 显示状态
|
||||||
checkHeaderVisibility();
|
checkHeaderVisibility();
|
||||||
@ -1198,7 +1211,7 @@ export function getMessageAreaScript(): string {
|
|||||||
div.appendChild(customContainer);
|
div.appendChild(customContainer);
|
||||||
|
|
||||||
messagesEl.appendChild(div);
|
messagesEl.appendChild(div);
|
||||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
smartScrollToBottom();
|
||||||
|
|
||||||
// 添加消息后检查 header 显示状态
|
// 添加消息后检查 header 显示状态
|
||||||
checkHeaderVisibility();
|
checkHeaderVisibility();
|
||||||
|
|||||||
250
src/views/modelSelector.ts
Normal file
250
src/views/modelSelector.ts
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
/**
|
||||||
|
* 模型选择器组件
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型选择器的 HTML 内容
|
||||||
|
*/
|
||||||
|
export function getModelSelectorContent(): string {
|
||||||
|
return `
|
||||||
|
<!-- 模型选择 -->
|
||||||
|
<div class="tooltip">
|
||||||
|
<div class="custom-select" id="modelSelect">
|
||||||
|
<div class="select-trigger" onclick="toggleModelDropdown()">
|
||||||
|
<span class="select-value" id="modelValue">Auto</span>
|
||||||
|
<svg class="select-arrow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M507.8 727.728a30.016 30.016 0 0 1-21.288-8.824L231.104 463.496a30.088 30.088 0 0 1 0-42.568 30.088 30.088 0 0 1 42.568 0l234.128 234.128 234.16-234.128a30.088 30.088 0 0 1 42.568 0 30.088 30.088 0 0 1 0 42.568L529.08 718.904a30 30 0 0 1-21.28 8.824z" fill="#8a8a8a"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="select-dropdown" id="modelDropdown">
|
||||||
|
<div class="select-option" 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>
|
||||||
|
<!-- 模型选择器的 tooltip 容器 -->
|
||||||
|
<div id="modelTooltip" class="model-tooltip"></div>
|
||||||
|
</div>
|
||||||
|
<span class="tooltiptext">选择模型</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型选择器的样式
|
||||||
|
*/
|
||||||
|
export function getModelSelectorStyles(): string {
|
||||||
|
return `
|
||||||
|
/* 自定义下拉框样式 */
|
||||||
|
.custom-select {
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.select-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
.select-trigger:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
.select-value {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.select-arrow {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
.custom-select.active .select-arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
.select-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 2px);
|
||||||
|
left: 0;
|
||||||
|
min-width: 100%;
|
||||||
|
background: var(--vscode-dropdown-background);
|
||||||
|
border: 1px solid var(--vscode-dropdown-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 1100;
|
||||||
|
display: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.custom-select.active .select-dropdown {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* 模型选择器的选项样式 */
|
||||||
|
#modelDropdown .select-option {
|
||||||
|
position: relative;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
#modelDropdown .select-option:hover {
|
||||||
|
background: rgba(128, 128, 128, 0.3);
|
||||||
|
}
|
||||||
|
#modelDropdown .select-option.selected {
|
||||||
|
background: rgba(128, 128, 128, 0.5);
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
/* 模型选择器的 tooltip 样式 */
|
||||||
|
.model-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型选择器的脚本
|
||||||
|
*/
|
||||||
|
export function getModelSelectorScript(): string {
|
||||||
|
return `
|
||||||
|
// 模型选择相关变量
|
||||||
|
let currentModel = 'auto';
|
||||||
|
|
||||||
|
// 切换模型下拉框显示/隐藏
|
||||||
|
function toggleModelDropdown() {
|
||||||
|
const modelSelect = document.getElementById('modelSelect');
|
||||||
|
const customSelect = document.getElementById('customSelect');
|
||||||
|
if (modelSelect) {
|
||||||
|
modelSelect.classList.toggle('active');
|
||||||
|
// 关闭模式下拉框
|
||||||
|
if (customSelect) {
|
||||||
|
customSelect.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择模型
|
||||||
|
function selectModel(value, label) {
|
||||||
|
currentModel = value;
|
||||||
|
const modelValue = document.getElementById('modelValue');
|
||||||
|
if (modelValue) {
|
||||||
|
modelValue.textContent = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新选中状态
|
||||||
|
const options = document.querySelectorAll('#modelDropdown .select-option');
|
||||||
|
options.forEach(option => {
|
||||||
|
if (option.getAttribute('data-value') === value) {
|
||||||
|
option.classList.add('selected');
|
||||||
|
} else {
|
||||||
|
option.classList.remove('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 关闭下拉框
|
||||||
|
const modelSelect = document.getElementById('modelSelect');
|
||||||
|
if (modelSelect) {
|
||||||
|
modelSelect.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击外部关闭模型下拉框
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
const modelSelect = document.getElementById('modelSelect');
|
||||||
|
if (modelSelect && !modelSelect.contains(event.target)) {
|
||||||
|
modelSelect.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取当前选中的模型
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -525,6 +525,37 @@ export function getWebviewContent(iconUri?: string): string {
|
|||||||
showQuestion(message.askId, message.question, message.options);
|
showQuestion(message.askId, message.question, message.options);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'conversationHistory':
|
||||||
|
// 渲染会话历史列表(支持分页)
|
||||||
|
renderConversationHistory({
|
||||||
|
items: message.items || [],
|
||||||
|
total: message.total || 0,
|
||||||
|
hasMore: message.hasMore || false
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'clearChat':
|
||||||
|
// 清空聊天界面
|
||||||
|
const messagesContainer = document.getElementById('messages');
|
||||||
|
if (messagesContainer) {
|
||||||
|
messagesContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'addUserMessage':
|
||||||
|
// 添加用户消息
|
||||||
|
if (message.text) {
|
||||||
|
addMessage(message.text, 'user');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'addAiMessage':
|
||||||
|
// 添加AI消息
|
||||||
|
if (message.text) {
|
||||||
|
addMessage(message.text, 'bot');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('[WebView] 未处理的消息类型:', message.command);
|
console.log('[WebView] 未处理的消息类型:', message.command);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user