feat:代码变更diff可视化功能实现
This commit is contained in:
4
.npmrc
4
.npmrc
@ -1 +1,3 @@
|
|||||||
enable-pre-post-scripts = true
|
enable-pre-post-scripts = true
|
||||||
|
shamefully-hoist = true
|
||||||
|
public-hoist-pattern[] = *
|
||||||
45
docs/code-changes-feature.md
Normal file
45
docs/code-changes-feature.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# 代码变更审查功能
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
AI 修改文件后,会在输入框上方显示"代码变更"面板,用户可以查看所有修改并选择采纳或拒绝。
|
||||||
|
|
||||||
|
## 核心文件
|
||||||
|
|
||||||
|
### 1. 数据结构
|
||||||
|
- `src/types/fileChanges.ts` - 变更数据类型定义
|
||||||
|
|
||||||
|
### 2. 服务层
|
||||||
|
- `src/services/changeTracker.ts` - 变更追踪服务(单例)
|
||||||
|
- `trackChange()` - 记录文件变更
|
||||||
|
- `acceptChange()` - 采纳变更(保存文件)
|
||||||
|
- `rejectChange()` - 拒绝变更(恢复旧内容)
|
||||||
|
|
||||||
|
### 3. UI 组件
|
||||||
|
- `src/views/changePanel.ts` - 变更面板 UI
|
||||||
|
- `src/utils/diffRenderer.ts` - Diff 可视化渲染
|
||||||
|
|
||||||
|
### 4. 集成点
|
||||||
|
- `src/utils/messageHandler.ts` - 消息处理
|
||||||
|
- `trackFileChange()` - 记录变更
|
||||||
|
- `handleAcceptChange()` - 处理采纳
|
||||||
|
- `handleRejectChange()` - 处理拒绝
|
||||||
|
- `sendChangesToWebview()` - 发送变更到前端
|
||||||
|
|
||||||
|
- `src/services/toolExecutor.ts` - 工具执行器
|
||||||
|
- 在 `executeFileWrite()` 中记录变更
|
||||||
|
|
||||||
|
## 使用流程
|
||||||
|
|
||||||
|
1. **开始对话** - 调用 `startChangeSession(sessionId)`
|
||||||
|
2. **修改文件** - 自动调用 `trackFileChange()`
|
||||||
|
3. **对话结束** - 调用 `sendChangesToWebview()` 显示变更面板
|
||||||
|
4. **用户操作** - 点击采纳/拒绝按钮
|
||||||
|
5. **处理结果** - 保存或恢复文件内容
|
||||||
|
|
||||||
|
## 待完成工作
|
||||||
|
|
||||||
|
1. 在 ICHelperPanel 中集成消息处理(监听 acceptChange/rejectChange 命令)
|
||||||
|
2. 在对话结束时调用 `sendChangesToWebview()`
|
||||||
|
3. 在 Webview 中实现变更列表的动态渲染
|
||||||
|
4. 处理前端的采纳/拒绝响应
|
||||||
50
docs/integration-guide.md
Normal file
50
docs/integration-guide.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# 代码变更审查功能 - 使用说明
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
AI 修改文件后,会在输入框上方显示"代码变更"面板,用户可以:
|
||||||
|
- 查看所有修改的文件列表
|
||||||
|
- 点击文件查看 diff 对比
|
||||||
|
- 采纳变更(保存文件)
|
||||||
|
- 拒绝变更(恢复旧内容)
|
||||||
|
|
||||||
|
## 已完成的集成
|
||||||
|
|
||||||
|
### 1. 后端集成
|
||||||
|
- ✅ 在 `ICHelperPanel.ts` 中添加了消息监听(acceptChange/rejectChange)
|
||||||
|
- ✅ 在发送消息时启动变更追踪会话
|
||||||
|
- ✅ 在文件操作时自动记录变更(messageHandler.ts、toolExecutor.ts)
|
||||||
|
|
||||||
|
### 2. 前端集成
|
||||||
|
- ✅ 在 `webviewContent.ts` 中添加了消息处理(showChanges/changeAccepted/changeRejected)
|
||||||
|
- ✅ 在 `changePanel.ts` 中实现了完整的 UI 交互逻辑
|
||||||
|
|
||||||
|
### 3. 核心功能
|
||||||
|
- ✅ 变更追踪服务(changeTracker.ts)
|
||||||
|
- ✅ Diff 可视化渲染(diffRenderer.ts)
|
||||||
|
- ✅ 采纳/拒绝变更逻辑
|
||||||
|
|
||||||
|
## 待完成工作
|
||||||
|
|
||||||
|
需要在对话结束时调用 `sendChangesToWebview(panel)` 来显示变更面板。
|
||||||
|
|
||||||
|
建议在以下位置添加:
|
||||||
|
1. 在 `handleUserMessage` 函数中,对话流结束时
|
||||||
|
2. 或在 `dialogManager` 的对话完成回调中
|
||||||
|
|
||||||
|
示例代码:
|
||||||
|
```typescript
|
||||||
|
// 对话结束时
|
||||||
|
import { sendChangesToWebview } from '../utils/messageHandler';
|
||||||
|
|
||||||
|
// 在对话完成的地方调用
|
||||||
|
sendChangesToWebview(panel);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
1. 启动插件(F5)
|
||||||
|
2. 发送消息让 AI 修改文件
|
||||||
|
3. 对话结束后,输入框上方应显示"代码变更"面板
|
||||||
|
4. 点击文件查看 diff
|
||||||
|
5. 点击"采纳"或"拒绝"按钮测试功能
|
||||||
@ -13,6 +13,10 @@ import {
|
|||||||
handlePlanAction,
|
handlePlanAction,
|
||||||
getCurrentTaskId,
|
getCurrentTaskId,
|
||||||
setLastTaskId,
|
setLastTaskId,
|
||||||
|
handleAcceptChange,
|
||||||
|
handleRejectChange,
|
||||||
|
startChangeSession,
|
||||||
|
handleOpenFileDiff,
|
||||||
} from "../utils/messageHandler";
|
} from "../utils/messageHandler";
|
||||||
import { compactDialog } from "../services/apiClient";
|
import { compactDialog } from "../services/apiClient";
|
||||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||||
@ -344,6 +348,10 @@ export async function showICHelperPanel(
|
|||||||
// 切换到当前面板的任务上下文
|
// 切换到当前面板的任务上下文
|
||||||
historyManager.switchToPanelTask(panelId);
|
historyManager.switchToPanelTask(panelId);
|
||||||
|
|
||||||
|
// 启动变更追踪会话
|
||||||
|
const sessionId = `session_${panelId}_${Date.now()}`;
|
||||||
|
startChangeSession(sessionId);
|
||||||
|
|
||||||
// 显示进度条
|
// 显示进度条
|
||||||
panel.webview.postMessage({ type: "showProgress" });
|
panel.webview.postMessage({ type: "showProgress" });
|
||||||
|
|
||||||
@ -476,6 +484,24 @@ export async function showICHelperPanel(
|
|||||||
// 退出登录
|
// 退出登录
|
||||||
vscode.commands.executeCommand("ic-coder.logout");
|
vscode.commands.executeCommand("ic-coder.logout");
|
||||||
break;
|
break;
|
||||||
|
case "acceptChange":
|
||||||
|
// 采纳变更
|
||||||
|
if (message.changeId) {
|
||||||
|
await handleAcceptChange(panel, message.changeId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "rejectChange":
|
||||||
|
// 拒绝变更
|
||||||
|
if (message.changeId) {
|
||||||
|
await handleRejectChange(panel, message.changeId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "openFileDiff":
|
||||||
|
// 打开文件 diff
|
||||||
|
if (message.changeId) {
|
||||||
|
await handleOpenFileDiff(panel, message.changeId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "checkInvitationCode":
|
case "checkInvitationCode":
|
||||||
// 检查邀请码验证状态
|
// 检查邀请码验证状态
|
||||||
{
|
{
|
||||||
|
|||||||
196
src/services/changeTracker.ts
Normal file
196
src/services/changeTracker.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* 文件变更追踪服务
|
||||||
|
* 功能:收集和管理 AI 修改文件的变更记录
|
||||||
|
* 依赖:types/fileChanges
|
||||||
|
* 使用场景:在文件操作时记录变更,供用户审查
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FileChange, ChangeSession } from '../types/fileChanges';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
class ChangeTrackerService {
|
||||||
|
private currentSession: ChangeSession | null = null;
|
||||||
|
private changeListeners: Array<(session: ChangeSession) => void> = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始新的变更会话
|
||||||
|
*/
|
||||||
|
startSession(sessionId: string): void {
|
||||||
|
this.currentSession = {
|
||||||
|
sessionId,
|
||||||
|
startTime: Date.now(),
|
||||||
|
changes: [],
|
||||||
|
status: 'active'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录文件变更
|
||||||
|
*/
|
||||||
|
trackChange(filePath: string, oldContent: string, newContent: string): string {
|
||||||
|
if (!this.currentSession) {
|
||||||
|
this.startSession(`session_${Date.now()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeId = `change_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
// 判断变更类型
|
||||||
|
let changeType: 'create' | 'modify' | 'delete';
|
||||||
|
if (oldContent === '' && newContent !== '') {
|
||||||
|
changeType = 'create';
|
||||||
|
} else if (oldContent !== '' && newContent === '') {
|
||||||
|
changeType = 'delete';
|
||||||
|
} else {
|
||||||
|
changeType = 'modify';
|
||||||
|
}
|
||||||
|
|
||||||
|
const change: FileChange = {
|
||||||
|
filePath,
|
||||||
|
oldContent,
|
||||||
|
newContent,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
changeType,
|
||||||
|
changeId
|
||||||
|
};
|
||||||
|
|
||||||
|
this.currentSession!.changes.push(change);
|
||||||
|
this.notifyListeners();
|
||||||
|
|
||||||
|
return changeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束当前会话
|
||||||
|
*/
|
||||||
|
endSession(): ChangeSession | null {
|
||||||
|
if (this.currentSession && this.currentSession.changes.length > 0) {
|
||||||
|
this.currentSession.status = 'completed';
|
||||||
|
const session = this.currentSession;
|
||||||
|
this.notifyListeners();
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
this.currentSession = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前会话
|
||||||
|
*/
|
||||||
|
getCurrentSession(): ChangeSession | null {
|
||||||
|
return this.currentSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空当前会话
|
||||||
|
*/
|
||||||
|
clearSession(): void {
|
||||||
|
this.currentSession = null;
|
||||||
|
this.notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除指定的变更
|
||||||
|
*/
|
||||||
|
removeChange(changeId: string): boolean {
|
||||||
|
if (!this.currentSession) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.currentSession.changes.findIndex(c => c.changeId === changeId);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.currentSession.changes.splice(index, 1);
|
||||||
|
this.notifyListeners();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听变更
|
||||||
|
*/
|
||||||
|
onChangeUpdate(listener: (session: ChangeSession) => void): void {
|
||||||
|
this.changeListeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知所有监听器
|
||||||
|
*/
|
||||||
|
private notifyListeners(): void {
|
||||||
|
if (this.currentSession) {
|
||||||
|
this.changeListeners.forEach(listener => listener(this.currentSession!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 采纳变更(保存文件)
|
||||||
|
*/
|
||||||
|
async acceptChange(changeId: string): Promise<boolean> {
|
||||||
|
if (!this.currentSession) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const change = this.currentSession.changes.find(c => c.changeId === changeId);
|
||||||
|
if (!change) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (!workspaceFolder) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePath = path.join(workspaceFolder.uri.fsPath, change.filePath);
|
||||||
|
await fs.promises.writeFile(absolutePath, change.newContent, 'utf-8');
|
||||||
|
|
||||||
|
this.removeChange(changeId);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ChangeTracker] 采纳变更失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拒绝变更(恢复旧内容)
|
||||||
|
*/
|
||||||
|
async rejectChange(changeId: string): Promise<boolean> {
|
||||||
|
if (!this.currentSession) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const change = this.currentSession.changes.find(c => c.changeId === changeId);
|
||||||
|
if (!change) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (!workspaceFolder) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePath = path.join(workspaceFolder.uri.fsPath, change.filePath);
|
||||||
|
|
||||||
|
// 如果是新建文件,删除它
|
||||||
|
if (change.changeType === 'create') {
|
||||||
|
if (fs.existsSync(absolutePath)) {
|
||||||
|
await fs.promises.unlink(absolutePath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 恢复旧内容
|
||||||
|
await fs.promises.writeFile(absolutePath, change.oldContent, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeChange(changeId);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ChangeTracker] 拒绝变更失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeTracker = new ChangeTrackerService();
|
||||||
@ -8,6 +8,8 @@ import * as os from 'os';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { readFileContent, readDirectory } from '../utils/readFiles';
|
import { readFileContent, readDirectory } from '../utils/readFiles';
|
||||||
import { createOrOverwriteFile } from '../utils/createFiles';
|
import { createOrOverwriteFile } from '../utils/createFiles';
|
||||||
|
import { resolveWorkspaceFilePath, showFileDiff } from '../utils/fileDiff';
|
||||||
|
import { changeTracker } from './changeTracker';
|
||||||
import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner';
|
import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner';
|
||||||
import { analyzeVcdFile } from '../utils/vcdParser';
|
import { analyzeVcdFile } from '../utils/vcdParser';
|
||||||
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
|
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
|
||||||
@ -125,8 +127,19 @@ async function executeFileRead(args: FileReadArgs): Promise<string> {
|
|||||||
* 执行 file_write 工具
|
* 执行 file_write 工具
|
||||||
*/
|
*/
|
||||||
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
||||||
|
const absolutePath = resolveWorkspaceFilePath(args.path);
|
||||||
|
const existedBeforeWrite = fs.existsSync(absolutePath);
|
||||||
|
const oldContent = existedBeforeWrite ? await readFileContent(args.path) : '';
|
||||||
|
|
||||||
await createOrOverwriteFile(args.path, args.content);
|
await createOrOverwriteFile(args.path, args.content);
|
||||||
|
|
||||||
|
// 记录文件变更
|
||||||
|
try {
|
||||||
|
changeTracker.trackChange(args.path, oldContent, args.content);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[ToolExecutor] 记录文件变更失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
// Verilog 文件添加知识图谱提示
|
// Verilog 文件添加知识图谱提示
|
||||||
const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv');
|
const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv');
|
||||||
if (isVerilogFile) {
|
if (isVerilogFile) {
|
||||||
|
|||||||
47
src/types/fileChanges.ts
Normal file
47
src/types/fileChanges.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* 文件变更追踪类型定义
|
||||||
|
* 功能:定义代码变更的数据结构
|
||||||
|
* 依赖:无
|
||||||
|
* 使用场景:AI 修改文件后的变更审查
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个文件的变更记录
|
||||||
|
*/
|
||||||
|
export interface FileChange {
|
||||||
|
/** 文件相对路径 */
|
||||||
|
filePath: string;
|
||||||
|
/** 修改前的内容 */
|
||||||
|
oldContent: string;
|
||||||
|
/** 修改后的内容 */
|
||||||
|
newContent: string;
|
||||||
|
/** 变更时间戳 */
|
||||||
|
timestamp: number;
|
||||||
|
/** 变更类型 */
|
||||||
|
changeType: 'create' | 'modify' | 'delete';
|
||||||
|
/** 变更 ID(唯一标识) */
|
||||||
|
changeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变更会话(一次对话的所有变更)
|
||||||
|
*/
|
||||||
|
export interface ChangeSession {
|
||||||
|
/** 会话 ID */
|
||||||
|
sessionId: string;
|
||||||
|
/** 会话开始时间 */
|
||||||
|
startTime: number;
|
||||||
|
/** 所有文件变更 */
|
||||||
|
changes: FileChange[];
|
||||||
|
/** 会话状态 */
|
||||||
|
status: 'active' | 'completed';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变更操作结果
|
||||||
|
*/
|
||||||
|
export interface ChangeActionResult {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
changeId: string;
|
||||||
|
}
|
||||||
184
src/utils/diffRenderer.ts
Normal file
184
src/utils/diffRenderer.ts
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* Diff 渲染工具
|
||||||
|
* 功能:生成代码差异的 HTML 展示
|
||||||
|
* 依赖:无
|
||||||
|
* 使用场景:在变更面板中展示文件修改的 diff
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface DiffLine {
|
||||||
|
type: 'add' | 'remove' | 'context';
|
||||||
|
content: string;
|
||||||
|
oldLineNumber?: number;
|
||||||
|
newLineNumber?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单的 diff 算法(基于行)
|
||||||
|
*/
|
||||||
|
export function generateDiff(oldContent: string, newContent: string): DiffLine[] {
|
||||||
|
const oldLines = oldContent.split('\n');
|
||||||
|
const newLines = newContent.split('\n');
|
||||||
|
const result: DiffLine[] = [];
|
||||||
|
|
||||||
|
let oldIndex = 0;
|
||||||
|
let newIndex = 0;
|
||||||
|
|
||||||
|
while (oldIndex < oldLines.length || newIndex < newLines.length) {
|
||||||
|
const oldLine = oldLines[oldIndex];
|
||||||
|
const newLine = newLines[newIndex];
|
||||||
|
|
||||||
|
if (oldIndex >= oldLines.length) {
|
||||||
|
// 只剩新行
|
||||||
|
result.push({
|
||||||
|
type: 'add',
|
||||||
|
content: newLine,
|
||||||
|
newLineNumber: newIndex + 1
|
||||||
|
});
|
||||||
|
newIndex++;
|
||||||
|
} else if (newIndex >= newLines.length) {
|
||||||
|
// 只剩旧行
|
||||||
|
result.push({
|
||||||
|
type: 'remove',
|
||||||
|
content: oldLine,
|
||||||
|
oldLineNumber: oldIndex + 1
|
||||||
|
});
|
||||||
|
oldIndex++;
|
||||||
|
} else if (oldLine === newLine) {
|
||||||
|
// 相同行
|
||||||
|
result.push({
|
||||||
|
type: 'context',
|
||||||
|
content: oldLine,
|
||||||
|
oldLineNumber: oldIndex + 1,
|
||||||
|
newLineNumber: newIndex + 1
|
||||||
|
});
|
||||||
|
oldIndex++;
|
||||||
|
newIndex++;
|
||||||
|
} else {
|
||||||
|
// 不同行,标记为删除和添加
|
||||||
|
result.push({
|
||||||
|
type: 'remove',
|
||||||
|
content: oldLine,
|
||||||
|
oldLineNumber: oldIndex + 1
|
||||||
|
});
|
||||||
|
result.push({
|
||||||
|
type: 'add',
|
||||||
|
content: newLine,
|
||||||
|
newLineNumber: newIndex + 1
|
||||||
|
});
|
||||||
|
oldIndex++;
|
||||||
|
newIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 diff 结果渲染为 HTML
|
||||||
|
*/
|
||||||
|
export function renderDiffHtml(diffLines: DiffLine[]): string {
|
||||||
|
let html = '<div class="diff-viewer">';
|
||||||
|
|
||||||
|
for (const line of diffLines) {
|
||||||
|
const lineClass = `diff-line diff-line-${line.type}`;
|
||||||
|
const oldNum = line.oldLineNumber ? `<span class="line-num">${line.oldLineNumber}</span>` : '<span class="line-num"></span>';
|
||||||
|
const newNum = line.newLineNumber ? `<span class="line-num">${line.newLineNumber}</span>` : '<span class="line-num"></span>';
|
||||||
|
const prefix = line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' ';
|
||||||
|
const escapedContent = escapeHtml(line.content);
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="${lineClass}">
|
||||||
|
${oldNum}
|
||||||
|
${newNum}
|
||||||
|
<span class="line-prefix">${prefix}</span>
|
||||||
|
<span class="line-content">${escapedContent}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转义 HTML 特殊字符
|
||||||
|
*/
|
||||||
|
function escapeHtml(text: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
return text.replace(/[&<>"']/g, m => map[m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 diff 样式
|
||||||
|
*/
|
||||||
|
export function getDiffStyles(): string {
|
||||||
|
return `
|
||||||
|
.diff-viewer {
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 0;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-add {
|
||||||
|
background: rgba(40, 167, 69, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-remove {
|
||||||
|
background: rgba(220, 53, 69, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-context {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-num {
|
||||||
|
display: inline-block;
|
||||||
|
width: 40px;
|
||||||
|
text-align: right;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: var(--vscode-editorLineNumber-foreground);
|
||||||
|
user-select: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-prefix {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-add .line-prefix {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-remove .line-prefix {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-content {
|
||||||
|
flex: 1;
|
||||||
|
padding-right: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
51
src/utils/fileDiff.ts
Normal file
51
src/utils/fileDiff.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import * as vscode from "vscode";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将相对路径解析为工作区绝对路径
|
||||||
|
*/
|
||||||
|
export function resolveWorkspaceFilePath(filePath: string): string {
|
||||||
|
if (path.isAbsolute(filePath)) {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
|
throw new Error("请先打开一个文件夹作为工作区,这样我就能为您修改文件了");
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 VS Code 原生 diff 视图展示文件修改前后对比
|
||||||
|
*/
|
||||||
|
export async function showFileDiff(
|
||||||
|
filePath: string,
|
||||||
|
oldContent: string,
|
||||||
|
title?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const absolutePath = resolveWorkspaceFilePath(filePath);
|
||||||
|
const fileUri = vscode.Uri.file(absolutePath);
|
||||||
|
const newBytes = await vscode.workspace.fs.readFile(fileUri);
|
||||||
|
const newContent = Buffer.from(newBytes).toString("utf-8");
|
||||||
|
|
||||||
|
if (oldContent === newContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = (await vscode.workspace.openTextDocument(fileUri)).languageId;
|
||||||
|
const oldDoc = await vscode.workspace.openTextDocument({
|
||||||
|
content: oldContent,
|
||||||
|
language,
|
||||||
|
});
|
||||||
|
|
||||||
|
const diffTitle = title || `${path.basename(absolutePath)} (修改前 <-> 修改后)`;
|
||||||
|
await vscode.commands.executeCommand(
|
||||||
|
"vscode.diff",
|
||||||
|
oldDoc.uri,
|
||||||
|
fileUri,
|
||||||
|
diffTitle,
|
||||||
|
{ preview: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -26,6 +26,9 @@ import {
|
|||||||
import { optimizePrompt } from "../services/promptOptimizeService";
|
import { optimizePrompt } from "../services/promptOptimizeService";
|
||||||
import { NotificationService } from "../services/notificationService";
|
import { NotificationService } from "../services/notificationService";
|
||||||
import { TrialExpirationService } from "../services/trialExpirationService";
|
import { TrialExpirationService } from "../services/trialExpirationService";
|
||||||
|
import { showFileDiff } from "./fileDiff";
|
||||||
|
import { changeTracker } from "../services/changeTracker";
|
||||||
|
import { generateDiff, renderDiffHtml } from "./diffRenderer";
|
||||||
|
|
||||||
import type { RunMode, ServiceTier } from "../types/api";
|
import type { RunMode, ServiceTier } from "../types/api";
|
||||||
|
|
||||||
@ -38,6 +41,14 @@ let currentSession: DialogSession | null = null;
|
|||||||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||||
let lastTaskId: string | null = null;
|
let lastTaskId: string | null = null;
|
||||||
|
|
||||||
|
async function trackFileChange(filePath: string, oldContent: string, newContent: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
changeTracker.trackChange(filePath, oldContent, newContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[MessageHandler] 记录文件变更失败:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理用户消息
|
* 处理用户消息
|
||||||
*/
|
*/
|
||||||
@ -372,6 +383,9 @@ async function handleUserMessageWithBackend(
|
|||||||
panel.reveal();
|
panel.reveal();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 发送代码变更到前端
|
||||||
|
sendChangesToWebview(panel);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[MessageHandler] 更新面板失败(面板可能已关闭):", error);
|
console.warn("[MessageHandler] 更新面板失败(面板可能已关闭):", error);
|
||||||
}
|
}
|
||||||
@ -774,11 +788,14 @@ async function handleFileOperation(
|
|||||||
if (!operation.searchText || !operation.replaceText) {
|
if (!operation.searchText || !operation.replaceText) {
|
||||||
throw new Error("缺少替换内容");
|
throw new Error("缺少替换内容");
|
||||||
}
|
}
|
||||||
|
const oldContentBeforeReplace = await readFileContent(operation.filePath);
|
||||||
await replaceFile(
|
await replaceFile(
|
||||||
operation.filePath,
|
operation.filePath,
|
||||||
operation.searchText,
|
operation.searchText,
|
||||||
operation.replaceText
|
operation.replaceText
|
||||||
);
|
);
|
||||||
|
const newContentAfterReplace = await readFileContent(operation.filePath);
|
||||||
|
await trackFileChange(operation.filePath, oldContentBeforeReplace, newContentAfterReplace);
|
||||||
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
|
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "receiveMessage",
|
command: "receiveMessage",
|
||||||
@ -914,7 +931,9 @@ export async function handleUpdateFile(
|
|||||||
content: string
|
content: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const oldContent = await readFileContent(filePath);
|
||||||
await updateFile(filePath, content);
|
await updateFile(filePath, content);
|
||||||
|
await trackFileChange(filePath, oldContent, content);
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "fileUpdated",
|
command: "fileUpdated",
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
@ -979,7 +998,10 @@ export async function handleReplaceInFile(
|
|||||||
replaceText: string
|
replaceText: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const oldContent = await readFileContent(filePath);
|
||||||
await replaceFile(filePath, searchText, replaceText);
|
await replaceFile(filePath, searchText, replaceText);
|
||||||
|
const newContent = await readFileContent(filePath);
|
||||||
|
await trackFileChange(filePath, oldContent, newContent);
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "fileReplaced",
|
command: "fileReplaced",
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
@ -1236,3 +1258,165 @@ export async function handleOptimizePrompt(
|
|||||||
vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`);
|
vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理采纳变更
|
||||||
|
*/
|
||||||
|
export async function handleAcceptChange(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
changeId: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const success = await changeTracker.acceptChange(changeId);
|
||||||
|
if (success) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeAccepted",
|
||||||
|
changeId: changeId,
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeAccepted",
|
||||||
|
changeId: changeId,
|
||||||
|
success: false,
|
||||||
|
error: "采纳变更失败"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[MessageHandler] 采纳变更失败:", error);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeAccepted",
|
||||||
|
changeId: changeId,
|
||||||
|
success: false,
|
||||||
|
error: String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理拒绝变更
|
||||||
|
*/
|
||||||
|
export async function handleRejectChange(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
changeId: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const success = await changeTracker.rejectChange(changeId);
|
||||||
|
if (success) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeRejected",
|
||||||
|
changeId: changeId,
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeRejected",
|
||||||
|
changeId: changeId,
|
||||||
|
success: false,
|
||||||
|
error: "拒绝变更失败"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[MessageHandler] 拒绝变更失败:", error);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "changeRejected",
|
||||||
|
changeId: changeId,
|
||||||
|
success: false,
|
||||||
|
error: String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在对话结束时发送变更列表到前端
|
||||||
|
*/
|
||||||
|
export function sendChangesToWebview(panel: vscode.WebviewPanel) {
|
||||||
|
const session = changeTracker.endSession();
|
||||||
|
if (session && session.changes.length > 0) {
|
||||||
|
const changesWithDiff = session.changes.map(change => {
|
||||||
|
const diffLines = generateDiff(change.oldContent, change.newContent);
|
||||||
|
const diffHtml = renderDiffHtml(diffLines);
|
||||||
|
return {
|
||||||
|
...change,
|
||||||
|
diffHtml
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "showChanges",
|
||||||
|
changes: changesWithDiff
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始新的变更会话
|
||||||
|
*/
|
||||||
|
export function startChangeSession(sessionId: string) {
|
||||||
|
changeTracker.startSession(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开文件 diff 编辑器
|
||||||
|
*/
|
||||||
|
export async function handleOpenFileDiff(
|
||||||
|
panel: vscode.WebviewPanel,
|
||||||
|
changeId: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const session = changeTracker.getCurrentSession();
|
||||||
|
if (!session) {
|
||||||
|
vscode.window.showErrorMessage('没有找到变更会话');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const change = session.changes.find(c => c.changeId === changeId);
|
||||||
|
if (!change) {
|
||||||
|
vscode.window.showErrorMessage('没有找到该变更');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (!workspaceFolder) {
|
||||||
|
vscode.window.showErrorMessage('没有打开的工作区');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建临时文件用于对比
|
||||||
|
const filePath = change.filePath;
|
||||||
|
const absolutePath = vscode.Uri.file(
|
||||||
|
path.join(workspaceFolder.uri.fsPath, filePath)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 创建虚拟文档显示旧内容
|
||||||
|
const oldUri = vscode.Uri.parse(
|
||||||
|
`ic-coder-diff:${filePath}.old?${changeId}`
|
||||||
|
).with({ scheme: 'ic-coder-diff' });
|
||||||
|
|
||||||
|
// 注册文档内容提供者(如果还没注册)
|
||||||
|
if (!(global as any).__diffProviderRegistered) {
|
||||||
|
const provider = new (class implements vscode.TextDocumentContentProvider {
|
||||||
|
provideTextDocumentContent(uri: vscode.Uri): string {
|
||||||
|
const changeId = uri.query;
|
||||||
|
const session = changeTracker.getCurrentSession();
|
||||||
|
const change = session?.changes.find(c => c.changeId === changeId);
|
||||||
|
return change?.oldContent || '';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
vscode.workspace.registerTextDocumentContentProvider('ic-coder-diff', provider);
|
||||||
|
(global as any).__diffProviderRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开 diff 编辑器
|
||||||
|
await vscode.commands.executeCommand(
|
||||||
|
'vscode.diff',
|
||||||
|
oldUri,
|
||||||
|
absolutePath,
|
||||||
|
`${filePath} (变更对比)`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MessageHandler] 打开 diff 失败:', error);
|
||||||
|
vscode.window.showErrorMessage(`打开 diff 失败: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
366
src/views/changePanel.ts
Normal file
366
src/views/changePanel.ts
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
/**
|
||||||
|
* 代码变更面板组件
|
||||||
|
* 功能:显示 AI 修改的文件列表和 diff 对比
|
||||||
|
* 依赖:utils/diffRenderer
|
||||||
|
* 使用场景:对话结束后展示代码变更供用户审查
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getDiffStyles } from '../utils/diffRenderer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取变更面板的 HTML 内容
|
||||||
|
*/
|
||||||
|
export function getChangePanelContent(): string {
|
||||||
|
return `
|
||||||
|
<div class="change-panel" id="changePanel" style="display: none;">
|
||||||
|
<div class="change-panel-header">
|
||||||
|
<div class="change-panel-title">
|
||||||
|
<span class="change-icon">📝</span>
|
||||||
|
<span>代码变更</span>
|
||||||
|
<span class="change-count" id="changeCount">0</span>
|
||||||
|
</div>
|
||||||
|
<button class="change-toggle-btn" id="changePanelToggle" onclick="toggleChangePanel()">
|
||||||
|
<span class="toggle-icon">▼</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="change-panel-body" id="changePanelBody" style="display: none;">
|
||||||
|
<div class="change-list" id="changeList">
|
||||||
|
<!-- 变更列表将动态插入 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取变更面板的样式
|
||||||
|
*/
|
||||||
|
export function getChangePanelStyles(): string {
|
||||||
|
return `
|
||||||
|
.change-panel {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-panel-header:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-panel-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-count {
|
||||||
|
background: var(--vscode-badge-background);
|
||||||
|
color: var(--vscode-badge-foreground);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-toggle-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-toggle-btn:hover {
|
||||||
|
background: var(--vscode-toolbar-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
display: inline-block;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon.expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-panel-body {
|
||||||
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item {
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item-header:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-type-badge {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-type-create {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-type-modify {
|
||||||
|
background: #ffc107;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-type-delete {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-file-path {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-action-btn {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-action-btn:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept-btn {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item-diff {
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
|
display: none;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-item-diff.expanded {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
${getDiffStyles()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取变更面板的脚本
|
||||||
|
*/
|
||||||
|
export function getChangePanelScript(): string {
|
||||||
|
return `
|
||||||
|
// 切换变更面板展开/收起
|
||||||
|
function toggleChangePanel() {
|
||||||
|
const body = document.getElementById('changePanelBody');
|
||||||
|
const toggleIcon = document.querySelector('.toggle-icon');
|
||||||
|
|
||||||
|
if (body.style.display === 'none') {
|
||||||
|
body.style.display = 'block';
|
||||||
|
toggleIcon.classList.add('expanded');
|
||||||
|
} else {
|
||||||
|
body.style.display = 'none';
|
||||||
|
toggleIcon.classList.remove('expanded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开文件 diff(在 VS Code 中打开)
|
||||||
|
function openFileDiff(changeId) {
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'openFileDiff',
|
||||||
|
changeId: changeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 采纳变更
|
||||||
|
function acceptChange(changeId) {
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'acceptChange',
|
||||||
|
changeId: changeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拒绝变更
|
||||||
|
function rejectChange(changeId) {
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'rejectChange',
|
||||||
|
changeId: changeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示变更面板(从后端接收变更列表)
|
||||||
|
window.showChangesPanel = function(changes) {
|
||||||
|
const changePanel = document.getElementById('changePanel');
|
||||||
|
const changeList = document.getElementById('changeList');
|
||||||
|
const changeCount = document.getElementById('changeCount');
|
||||||
|
|
||||||
|
if (!changePanel || !changeList || !changeCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新变更数量
|
||||||
|
changeCount.textContent = changes.length;
|
||||||
|
|
||||||
|
// 清空现有列表
|
||||||
|
changeList.innerHTML = '';
|
||||||
|
|
||||||
|
// 渲染每个变更项
|
||||||
|
changes.forEach(change => {
|
||||||
|
const changeItem = createChangeItem(change);
|
||||||
|
changeList.appendChild(changeItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示面板
|
||||||
|
changePanel.style.display = 'block';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建单个变更项的 DOM 元素
|
||||||
|
function createChangeItem(change) {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'change-item';
|
||||||
|
item.id = 'change-item-' + change.changeId;
|
||||||
|
|
||||||
|
const typeLabel = change.changeType === 'create' ? '新建' :
|
||||||
|
change.changeType === 'modify' ? '修改' : '删除';
|
||||||
|
|
||||||
|
item.innerHTML = \`
|
||||||
|
<div class="change-item-header" onclick="openFileDiff('\${change.changeId}')">
|
||||||
|
<div class="change-item-info">
|
||||||
|
<span class="change-type-badge change-type-\${change.changeType}">\${typeLabel}</span>
|
||||||
|
<span class="change-file-path">\${change.filePath}</span>
|
||||||
|
</div>
|
||||||
|
<div class="change-item-actions">
|
||||||
|
<button class="change-action-btn accept-btn" onclick="event.stopPropagation(); acceptChange('\${change.changeId}')">采纳</button>
|
||||||
|
<button class="change-action-btn reject-btn" onclick="event.stopPropagation(); rejectChange('\${change.changeId}')">拒绝</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理采纳变更的响应
|
||||||
|
window.handleChangeAccepted = function(changeId, success, error) {
|
||||||
|
if (success) {
|
||||||
|
// 从列表中移除该变更项
|
||||||
|
const item = document.getElementById('change-item-' + changeId);
|
||||||
|
if (item) {
|
||||||
|
item.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新变更数量
|
||||||
|
updateChangeCount();
|
||||||
|
} else {
|
||||||
|
console.error('采纳变更失败:', error);
|
||||||
|
alert('采纳变更失败: ' + (error || '未知错误'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理拒绝变更的响应
|
||||||
|
window.handleChangeRejected = function(changeId, success, error) {
|
||||||
|
if (success) {
|
||||||
|
// 从列表中移除该变更项
|
||||||
|
const item = document.getElementById('change-item-' + changeId);
|
||||||
|
if (item) {
|
||||||
|
item.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新变更数量
|
||||||
|
updateChangeCount();
|
||||||
|
} else {
|
||||||
|
console.error('拒绝变更失败:', error);
|
||||||
|
alert('拒绝变更失败: ' + (error || '未知错误'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新变更数量
|
||||||
|
function updateChangeCount() {
|
||||||
|
const changeList = document.getElementById('changeList');
|
||||||
|
const changeCount = document.getElementById('changeCount');
|
||||||
|
const changePanel = document.getElementById('changePanel');
|
||||||
|
|
||||||
|
if (changeList && changeCount) {
|
||||||
|
const count = changeList.children.length;
|
||||||
|
changeCount.textContent = count;
|
||||||
|
|
||||||
|
// 如果没有变更了,隐藏面板
|
||||||
|
if (count === 0 && changePanel) {
|
||||||
|
changePanel.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -34,6 +34,11 @@ import {
|
|||||||
getExampleShowcaseStyles,
|
getExampleShowcaseStyles,
|
||||||
getExampleShowcaseScript,
|
getExampleShowcaseScript,
|
||||||
} from "./exampleShowcase";
|
} from "./exampleShowcase";
|
||||||
|
import {
|
||||||
|
getChangePanelContent,
|
||||||
|
getChangePanelStyles,
|
||||||
|
getChangePanelScript,
|
||||||
|
} from "./changePanel";
|
||||||
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,6 +54,8 @@ export function getInputAreaContent(
|
|||||||
<div class="input-area centered" id="inputArea">
|
<div class="input-area centered" id="inputArea">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
|
<!-- 代码变更面板 -->
|
||||||
|
${getChangePanelContent()}
|
||||||
<!-- 顶部工具栏 -->
|
<!-- 顶部工具栏 -->
|
||||||
<div class="input-top-toolbar">
|
<div class="input-top-toolbar">
|
||||||
${getContextButtonContent()}
|
${getContextButtonContent()}
|
||||||
@ -94,6 +101,7 @@ export function getInputAreaStyles(): string {
|
|||||||
${getContextCompressStyles()}
|
${getContextCompressStyles()}
|
||||||
${getOptimizeButtonStyles()}
|
${getOptimizeButtonStyles()}
|
||||||
${getExampleShowcaseStyles()}
|
${getExampleShowcaseStyles()}
|
||||||
|
${getChangePanelStyles()}
|
||||||
.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;
|
||||||
@ -300,6 +308,7 @@ export function getInputAreaScript(): string {
|
|||||||
${getContextButtonScript()}
|
${getContextButtonScript()}
|
||||||
${getContextCompressScript()}
|
${getContextCompressScript()}
|
||||||
${getOptimizeButtonScript()}
|
${getOptimizeButtonScript()}
|
||||||
|
${getChangePanelScript()}
|
||||||
|
|
||||||
// 对话状态管理
|
// 对话状态管理
|
||||||
let isConversationActive = false;
|
let isConversationActive = false;
|
||||||
|
|||||||
@ -882,6 +882,27 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'showChanges':
|
||||||
|
// 显示代码变更
|
||||||
|
if (typeof showChangesPanel === 'function') {
|
||||||
|
showChangesPanel(message.changes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'changeAccepted':
|
||||||
|
// 变更已采纳
|
||||||
|
if (typeof handleChangeAccepted === 'function') {
|
||||||
|
handleChangeAccepted(message.changeId, message.success, message.error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'changeRejected':
|
||||||
|
// 变更已拒绝
|
||||||
|
if (typeof handleChangeRejected === 'function') {
|
||||||
|
handleChangeRejected(message.changeId, message.success, message.error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('[WebView] 未处理的消息类型:', message.command);
|
console.log('[WebView] 未处理的消息类型:', message.command);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user