feat:代码变更diff可视化功能实现

This commit is contained in:
Roe-xin
2026-03-02 10:00:04 +08:00
parent 3e18299099
commit 4c7ec65577
13 changed files with 1195 additions and 1 deletions

2
.npmrc
View File

@ -1 +1,3 @@
enable-pre-post-scripts = true
shamefully-hoist = true
public-hoist-pattern[] = *

View 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
View 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. 点击"采纳"或"拒绝"按钮测试功能

View File

@ -13,6 +13,10 @@ import {
handlePlanAction,
getCurrentTaskId,
setLastTaskId,
handleAcceptChange,
handleRejectChange,
startChangeSession,
handleOpenFileDiff,
} from "../utils/messageHandler";
import { compactDialog } from "../services/apiClient";
import { VCDViewerPanel } from "./VCDViewerPanel";
@ -344,6 +348,10 @@ export async function showICHelperPanel(
// 切换到当前面板的任务上下文
historyManager.switchToPanelTask(panelId);
// 启动变更追踪会话
const sessionId = `session_${panelId}_${Date.now()}`;
startChangeSession(sessionId);
// 显示进度条
panel.webview.postMessage({ type: "showProgress" });
@ -476,6 +484,24 @@ export async function showICHelperPanel(
// 退出登录
vscode.commands.executeCommand("ic-coder.logout");
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":
// 检查邀请码验证状态
{

View 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();

View File

@ -8,6 +8,8 @@ import * as os from 'os';
import * as fs from 'fs';
import { readFileContent, readDirectory } from '../utils/readFiles';
import { createOrOverwriteFile } from '../utils/createFiles';
import { resolveWorkspaceFilePath, showFileDiff } from '../utils/fileDiff';
import { changeTracker } from './changeTracker';
import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner';
import { analyzeVcdFile } from '../utils/vcdParser';
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
@ -125,8 +127,19 @@ async function executeFileRead(args: FileReadArgs): Promise<string> {
* 执行 file_write 工具
*/
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);
// 记录文件变更
try {
changeTracker.trackChange(args.path, oldContent, args.content);
} catch (error) {
console.warn('[ToolExecutor] 记录文件变更失败:', error);
}
// Verilog 文件添加知识图谱提示
const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv');
if (isVerilogFile) {

47
src/types/fileChanges.ts Normal file
View 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
View 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> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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
View 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 }
);
}

View File

@ -26,6 +26,9 @@ import {
import { optimizePrompt } from "../services/promptOptimizeService";
import { NotificationService } from "../services/notificationService";
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";
@ -38,6 +41,14 @@ let currentSession: DialogSession | null = null;
/** 最后一个活跃的 taskId用于压缩等操作 */
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();
}
);
// 发送代码变更到前端
sendChangesToWebview(panel);
} catch (error) {
console.warn("[MessageHandler] 更新面板失败(面板可能已关闭):", error);
}
@ -774,11 +788,14 @@ async function handleFileOperation(
if (!operation.searchText || !operation.replaceText) {
throw new Error("缺少替换内容");
}
const oldContentBeforeReplace = await readFileContent(operation.filePath);
await replaceFile(
operation.filePath,
operation.searchText,
operation.replaceText
);
const newContentAfterReplace = await readFileContent(operation.filePath);
await trackFileChange(operation.filePath, oldContentBeforeReplace, newContentAfterReplace);
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
panel.webview.postMessage({
command: "receiveMessage",
@ -914,7 +931,9 @@ export async function handleUpdateFile(
content: string
) {
try {
const oldContent = await readFileContent(filePath);
await updateFile(filePath, content);
await trackFileChange(filePath, oldContent, content);
panel.webview.postMessage({
command: "fileUpdated",
filePath: filePath,
@ -979,7 +998,10 @@ export async function handleReplaceInFile(
replaceText: string
) {
try {
const oldContent = await readFileContent(filePath);
await replaceFile(filePath, searchText, replaceText);
const newContent = await readFileContent(filePath);
await trackFileChange(filePath, oldContent, newContent);
panel.webview.postMessage({
command: "fileReplaced",
filePath: filePath,
@ -1236,3 +1258,165 @@ export async function handleOptimizePrompt(
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
View 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';
}
}
}
`;
}

View File

@ -34,6 +34,11 @@ import {
getExampleShowcaseStyles,
getExampleShowcaseScript,
} from "./exampleShowcase";
import {
getChangePanelContent,
getChangePanelStyles,
getChangePanelScript,
} from "./changePanel";
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
/**
@ -49,6 +54,8 @@ export function getInputAreaContent(
<div class="input-area centered" id="inputArea">
<div class="input-group">
<div class="input-wrapper">
<!-- 代码变更面板 -->
${getChangePanelContent()}
<!-- 顶部工具栏 -->
<div class="input-top-toolbar">
${getContextButtonContent()}
@ -94,6 +101,7 @@ export function getInputAreaStyles(): string {
${getContextCompressStyles()}
${getOptimizeButtonStyles()}
${getExampleShowcaseStyles()}
${getChangePanelStyles()}
.input-area {
border-top: 1px solid var(--vscode-panel-border);
padding-top: 15px;
@ -300,6 +308,7 @@ export function getInputAreaScript(): string {
${getContextButtonScript()}
${getContextCompressScript()}
${getOptimizeButtonScript()}
${getChangePanelScript()}
// 对话状态管理
let isConversationActive = false;

View File

@ -882,6 +882,27 @@ export function getWebviewContent(
}
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:
console.log('[WebView] 未处理的消息类型:', message.command);
}