-
-
+
+
@@ -198,7 +189,6 @@ export class ReviewPanel {
-
+
`;
}
diff --git a/src/render/statusBar.ts b/src/render/statusBar.ts
index c973111..4284084 100644
--- a/src/render/statusBar.ts
+++ b/src/render/statusBar.ts
@@ -1,70 +1,127 @@
/**
- * 状态栏管理器 - 显示 Diff 状态信息
+ * 状态栏管理器 — 常驻显示
+ *
+ * 始终在左侧状态栏显示 AI Diff 状态:
+ * - 有 pending 变更: $(diff) AI Diff: N文件 M编辑 (黄色) — 点击打开文件列表
+ * - Diff 活跃: 右侧额外显示 Accept/Reject 按钮
+ * - 无变更: $(diff) AI Diff (暗色) — 点击打开文件列表
*/
import * as vscode from 'vscode';
-import { Snapshot } from '../models/types';
-import { STATUS_BAR_PRIORITY } from '../models/constants';
+import { ChangeSetManager } from '../snapshot/snapshotManager';
+import { DiffViewer, DiffSession } from './diffViewer';
+import { COMMANDS } from '../models/constants';
export class StatusBarManager {
- private statusBarItem: vscode.StatusBarItem;
- private chunkCountItem: vscode.StatusBarItem;
+ /** ★ 常驻摘要按钮 */
+ private summaryItem: vscode.StatusBarItem;
+ /** Diff 模式 Accept 按钮 */
+ private acceptBtn: vscode.StatusBarItem;
+ /** Diff 模式 Reject 按钮 */
+ private rejectBtn: vscode.StatusBarItem;
+ /** Diff 模式 Accept All 按钮 */
+ private acceptAllBtn: vscode.StatusBarItem;
- constructor() {
- this.statusBarItem = vscode.window.createStatusBarItem(
+ constructor(
+ private changeSetManager: ChangeSetManager,
+ private diffViewer: DiffViewer
+ ) {
+ // ★ 常驻摘要 — 始终显示
+ this.summaryItem = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Left,
- STATUS_BAR_PRIORITY.DIFF_STATUS
+ 100
);
+ this.summaryItem.command = COMMANDS.SHOW_DIFF_PANEL;
+ this.summaryItem.text = '$(diff) AI Diff';
+ this.summaryItem.tooltip = 'AI Diff — 点击查看变更列表';
+ this.summaryItem.show();
- this.chunkCountItem = vscode.window.createStatusBarItem(
- vscode.StatusBarAlignment.Left,
- STATUS_BAR_PRIORITY.CHUNK_COUNT
+ // Accept 按钮
+ this.acceptBtn = vscode.window.createStatusBarItem(
+ vscode.StatusBarAlignment.Right,
+ 100
);
+ this.acceptBtn.command = 'aiDiffPreview.acceptCurrentDiff';
+
+ // Reject 按钮
+ this.rejectBtn = vscode.window.createStatusBarItem(
+ vscode.StatusBarAlignment.Right,
+ 99
+ );
+ this.rejectBtn.command = 'aiDiffPreview.rejectCurrentDiff';
+
+ // Accept All 按钮
+ this.acceptAllBtn = vscode.window.createStatusBarItem(
+ vscode.StatusBarAlignment.Right,
+ 98
+ );
+ this.acceptAllBtn.command = 'aiDiffPreview.acceptAllFromDiff';
}
- /**
- * 更新状态栏
- */
- update(snapshot: Snapshot): void {
- const pendingChunks = snapshot.chunks.filter(c => c.status === 'pending');
- const conflictChunks = snapshot.chunks.filter(c => c.status === 'conflict');
+ enterDiffMode(session: DiffSession): void {
+ const label = session.editId ? '编辑' : '文件';
+ this.acceptBtn.text = `$(check) Accept 本次${label}变更`;
+ this.acceptBtn.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
+ this.acceptBtn.tooltip = `接受此${label}的变更并跳转到下一个`;
+ this.acceptBtn.show();
- // 更新主状态
- this.statusBarItem.text = '$(diff) AI Diff';
- this.statusBarItem.tooltip = 'AI Diff Preview 活跃';
- this.statusBarItem.show();
+ this.rejectBtn.text = `$(close) Reject 本次${label}变更`;
+ this.rejectBtn.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
+ this.rejectBtn.tooltip = `拒绝此${label}的变更并跳转到下一个`;
+ this.rejectBtn.show();
- // 更新变更块数量
- if (pendingChunks.length > 0 || conflictChunks.length > 0) {
- this.chunkCountItem.text = `$(edit) ${pendingChunks.length} 个变更`;
- if (conflictChunks.length > 0) {
- this.chunkCountItem.text += ` (${conflictChunks.length} 个冲突)`;
- this.chunkCountItem.backgroundColor = new vscode.ThemeColor(
- 'statusBarItem.warningBackground'
- );
- } else {
- this.chunkCountItem.backgroundColor = undefined;
- }
- this.chunkCountItem.tooltip = '点击查看变更详情';
- this.chunkCountItem.show();
+ this.acceptAllBtn.text = '$(check-all) 接受此文件全部';
+ this.acceptAllBtn.tooltip = '接受此文件的所有编辑';
+ this.acceptAllBtn.show();
+ }
+
+ exitDiffMode(): void {
+ this.acceptBtn.hide();
+ this.rejectBtn.hide();
+ this.acceptAllBtn.hide();
+ }
+
+ /** ★ 常驻刷新 */
+ refresh(): void {
+ const session = this.diffViewer.getActiveSession();
+ if (session) {
+ this.enterDiffMode(session);
} else {
- this.chunkCountItem.hide();
+ this.exitDiffMode();
}
+
+ const set = this.changeSetManager.getCurrentSet();
+ if (!set) {
+ // ★ 无变更集也常驻显示
+ this.summaryItem.text = '$(diff) AI Diff';
+ this.summaryItem.tooltip = 'AI Diff — 等待变更';
+ this.summaryItem.backgroundColor = undefined;
+ this.summaryItem.show();
+ return;
+ }
+
+ const pendingFiles = set.changes.filter(c => c.status === 'pending');
+ const totalEdits = pendingFiles.reduce(
+ (sum, c) => sum + c.edits.filter(e => e.status === 'pending').length, 0
+ );
+
+ if (pendingFiles.length === 0 && set.changes.length > 0) {
+ this.summaryItem.text = '$(diff) AI Diff: 已完成';
+ this.summaryItem.tooltip = '所有变更已处理';
+ this.summaryItem.backgroundColor = undefined;
+ } else if (pendingFiles.length > 0) {
+ this.summaryItem.text = `$(diff) AI Diff: ${pendingFiles.length}文件 ${totalEdits}编辑`;
+ this.summaryItem.tooltip = `点击查看变更列表\n${pendingFiles.map(c => `• ${c.filePath} (${c.edits.length}次)`).join('\n')}`;
+ this.summaryItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
+ }
+
+ this.summaryItem.show();
}
- /**
- * 清除状态栏
- */
- clear(): void {
- this.statusBarItem.hide();
- this.chunkCountItem.hide();
- }
-
- /**
- * 释放资源
- */
dispose(): void {
- this.statusBarItem.dispose();
- this.chunkCountItem.dispose();
+ this.summaryItem.dispose();
+ this.acceptBtn.dispose();
+ this.rejectBtn.dispose();
+ this.acceptAllBtn.dispose();
}
-}
+}
\ No newline at end of file
diff --git a/src/snapshot/snapshotManager.ts b/src/snapshot/snapshotManager.ts
index aa19fb2..52c535b 100644
--- a/src/snapshot/snapshotManager.ts
+++ b/src/snapshot/snapshotManager.ts
@@ -19,7 +19,6 @@ function norm(p: string): string {
export class ChangeSetManager {
private currentSet: ChangeSet | null = null;
- /** 原始文件内容快照(每个文件第一次编辑前捕获) */
private originalContents: Map
= new Map();
startNewSet(): string {
@@ -49,59 +48,43 @@ export class ChangeSetManager {
}
/**
- * ★ 记录一次文件编辑(重构后)
+ * ★ 记录一次文件编辑
*
- * 核心逻辑:
- * 1. 每次 Edit/Write 创建独立 EditRecord
- * 2. 如果该文件已有 pending 的 FileChange → 追加 EditRecord
- * 3. 如果该文件的 FileChange 已 accepted/rejected → 创建新 FileChange(修复 Bug)
- * 4. 如果该文件没有 FileChange → 创建新 FileChange
+ * 合并规则:
+ * - 优先找同文件 pending 的 FileChange → 追加 EditRecord,更新聚合 diff
+ * - 没有 pending 的 → 创建新 FileChange(首次编辑 / 前轮已处理完)
*/
recordEdit(filePath: string, toolName: string, oldString: string, newString: string, _content: string): void {
- if (!this.currentSet) {
- this.startNewSet();
- }
+ if (!this.currentSet) this.startNewSet();
filePath = norm(filePath);
const currentContent = this.readFile(filePath) || '';
- const now = Date.now();
- console.log(`[AI Diff] recordEdit: ${toolName} → ${filePath}`);
+ // ★ 优先匹配 pending 的 FileChange
+ const sameFileChanges = this.currentSet!.changes.filter(c => norm(c.filePath) === filePath);
+ const pendingChange = sameFileChanges.find(c => c.status === 'pending');
+ const hasNonPending = sameFileChanges.some(c => c.status !== 'pending');
- // Step 1: 查找该文件是否已有变更记录
- const existingChange = this.currentSet!.changes.find(c => norm(c.filePath) === filePath);
+ console.log(`[AI Diff] recordEdit: ${toolName} → ${filePath} (pending=${!!pendingChange}, nonPending=${hasNonPending})`);
- // ★ 如果已有变更记录且已 accepted/rejected,需要重置缓存以重新捕获
- if (existingChange && existingChange.status !== 'pending') {
- this.originalContents.delete(filePath);
- }
+ // 没有 pending → 清除原始缓存重新捕获
+ if (!pendingChange) this.originalContents.delete(filePath);
- // Step 2: 确保已捕获原始内容
const capturedOriginal = this.captureOriginalContent(filePath, toolName, oldString, newString, currentContent);
- // Step 3: 确定本次编辑的 beforeContent
+ // 确定 beforeContent
let beforeContent: string;
-
- if (existingChange) {
- const lastEdit = existingChange.edits[existingChange.edits.length - 1];
- if (existingChange.status === 'pending') {
- // 还在收集中,before 使用上一次编辑的 afterContent
- beforeContent = lastEdit.afterContent;
- } else {
- // ★ Bug 修复:已 accepted/rejected,需要新的 beforeContent
- // 重新捕获:当前文件内容 = 本次编辑后的结果
- beforeContent = this.reverseEdit(currentContent, oldString, newString);
- }
+ if (pendingChange) {
+ // ★ 合并:before = 上一次编辑后的文件状态
+ beforeContent = pendingChange.latestContent;
} else {
- // 首次编辑该文件
+ // 新追踪:before = 从当前文件反推的原始内容
beforeContent = capturedOriginal;
}
- // Step 3: 计算本次编辑的独立 Diff
const afterContent = currentContent;
const editDiffLines = this.computeDiffLines(beforeContent, afterContent);
- // Step 4: 创建 EditRecord
const editRecord: EditRecord = {
id: uuidv4(),
filePath,
@@ -112,23 +95,18 @@ export class ChangeSetManager {
newString: newString || '',
diffLines: editDiffLines,
status: 'pending',
- timestamp: now,
+ timestamp: Date.now(),
};
- // Step 5: 决定 FileChange 策略
- if (existingChange && existingChange.status === 'pending') {
- // 追加到已有 FileChange
- existingChange.edits.push(editRecord);
- existingChange.latestContent = afterContent;
- existingChange.diffLines = this.computeDiffLines(existingChange.originalContent, afterContent);
- console.log(`[AI Diff] 追加 EditRecord 到已有 FileChange: ${filePath}, edits=${existingChange.edits.length}`);
+ if (pendingChange) {
+ // ★ 追加到已有 pending FileChange,更新聚合 diff
+ pendingChange.edits.push(editRecord);
+ pendingChange.latestContent = afterContent;
+ pendingChange.diffLines = this.computeDiffLines(pendingChange.originalContent, afterContent);
} else {
- // 创建新 FileChange(首次编辑 或 之前的已 accepted/rejected)
- const changeType: FileChangeType = (toolName === 'Write' && !capturedOriginal)
- ? 'create'
- : 'modify';
-
- const fileChange: FileChange = {
+ // ★ 新建 FileChange
+ const changeType: FileChangeType = (toolName === 'Write' && !capturedOriginal) ? 'create' : 'modify';
+ const fc: FileChange = {
id: uuidv4(),
filePath,
type: changeType,
@@ -138,85 +116,57 @@ export class ChangeSetManager {
diffLines: editDiffLines,
status: 'pending',
};
-
- this.currentSet!.changes.push(fileChange);
- if (existingChange) {
- console.log(`[AI Diff] ★ 新建 FileChange(旧 FileChange 已 ${existingChange.status}): ${filePath}`);
- } else {
- console.log(`[AI Diff] 新建 FileChange: ${filePath}, type=${changeType}`);
- }
+ this.currentSet!.changes.push(fc);
}
}
- /**
- * 捕获文件的原始内容
- */
private captureOriginalContent(
- filePath: string,
- toolName: string,
- oldString: string,
- newString: string,
- currentContent: string
+ filePath: string, toolName: string, oldString: string, newString: string, currentContent: string
): string {
if (this.originalContents.has(filePath)) {
return this.originalContents.get(filePath)!;
}
-
let original: string;
-
if (toolName === 'Write') {
- // Write: 无法获取旧内容
original = '';
} else if (toolName === 'Edit' && oldString) {
- // Edit: 从当前文件中反推原始内容
const idx = currentContent.indexOf(newString);
if (idx !== -1) {
original = currentContent.substring(0, idx) + oldString + currentContent.substring(idx + newString.length);
} else {
- // newString 不在文件中,可能已被之前的编辑影响
original = oldString;
}
} else {
original = currentContent;
}
-
this.originalContents.set(filePath, original);
console.log(`[AI Diff] 捕获原始内容: ${filePath} (${original.length} 字符)`);
return original;
}
- /**
- * 反推编辑前的内容(用于已 accepted/rejected 后新编辑的场景)
- * currentContent = 编辑后内容,reverseEdit = 从后往前还原
- */
private reverseEdit(currentContent: string, oldString: string, newString: string): string {
if (!newString) return currentContent;
const idx = currentContent.indexOf(newString);
if (idx !== -1) {
- const before = currentContent.substring(0, idx) + oldString + currentContent.substring(idx + newString.length);
- return before;
+ return currentContent.substring(0, idx) + oldString + currentContent.substring(idx + newString.length);
}
return currentContent;
}
// ---- 状态更新 ----
- /** 更新单个编辑记录的状态 */
updateEditStatus(fileId: string, editId: string, status: ChangeStatus): void {
if (!this.currentSet) return;
const change = this.currentSet.changes.find(c => c.id === fileId);
if (!change) return;
const edit = change.edits.find(e => e.id === editId);
if (edit) edit.status = status;
-
- // 如果该文件所有编辑都已处理,更新文件状态
if (change.edits.every(e => e.status !== 'pending')) {
const allAccepted = change.edits.every(e => e.status === 'accepted');
change.status = allAccepted ? 'accepted' : 'rejected';
}
}
- /** 更新整个文件的状态(同时更新所有编辑) */
updateFileStatus(fileId: string, status: ChangeStatus): void {
if (!this.currentSet) return;
const change = this.currentSet.changes.find(c => c.id === fileId);
@@ -234,132 +184,132 @@ export class ChangeSetManager {
});
}
- // ---- Accept(应用到磁盘) ----
+ // ---- Accept ----
- /**
- * 接受单个编辑 — 内容已在磁盘上,只需标记状态
- */
acceptEditRecord(fileId: string, editId: string): boolean {
- this.updateEditStatus(fileId, editId, 'accepted');
const change = this.findChange(fileId);
if (!change) return false;
-
- // 如果整个文件所有编辑都已接受,写入最终的 latestContent
+ const edit = change.edits.find(e => e.id === editId);
+ if (!edit) return false;
+ // 此 edit 变更已在磁盘上,只需标记状态
+ this.updateEditStatus(fileId, editId, 'accepted');
if (change.edits.every(e => e.status === 'accepted')) {
return this.writeFile(change.filePath, change.latestContent);
}
return true;
}
- /**
- * 接受整个文件的所有编辑
- */
+ /** ★ 接受整个文件:先写入再标记 */
acceptFile(fileId: string): boolean {
- this.updateFileStatus(fileId, 'accepted');
const change = this.findChange(fileId);
if (!change) return false;
- return this.writeFile(change.filePath, change.latestContent);
+ const ok = this.writeFile(change.filePath, change.latestContent);
+ if (!ok) return false;
+ this.updateFileStatus(fileId, 'accepted');
+ return true;
}
- /**
- * 接受所有变更
- */
+ /** ★ 接受所有 pending 的文件 */
acceptAll(): number {
if (!this.currentSet) return 0;
+ // snapshot pending list before looping (loop won't affect already-processed items)
+ const pendingIds = this.currentSet.changes.filter(c => c.status === 'pending').map(c => c.id);
let count = 0;
- for (const change of this.currentSet.changes) {
- if (change.status === 'pending' && this.acceptFile(change.id)) {
- count++;
- }
+ for (const id of pendingIds) {
+ if (this.acceptFile(id)) count++;
}
return count;
}
- // ---- Reject(恢复原始内容) ----
+ // ---- Reject ----
- /**
- * 拒绝单个编辑 — 从磁盘文件中还原该编辑的变更
- */
rejectEditRecord(fileId: string, editId: string): boolean {
const change = this.findChange(fileId);
if (!change) return false;
-
const edit = change.edits.find(e => e.id === editId);
if (!edit) return false;
- // 从当前文件中还原该编辑:将 newString 替换回 oldString
+ let reverted: string;
try {
const currentContent = this.readFile(change.filePath);
- if (currentContent !== null) {
- const revertedContent = this.revertEditInContent(currentContent, edit);
- this.writeFile(change.filePath, revertedContent);
- }
+ if (currentContent === null) return false;
+ reverted = this.revertEditInContent(currentContent, edit);
} catch (e) {
console.error(`[AI Diff] 还原编辑失败: ${change.filePath}`, e);
return false;
}
+ const ok = this.writeFile(change.filePath, reverted);
+ if (!ok) return false;
this.updateEditStatus(fileId, editId, 'rejected');
return true;
}
- /**
- * 拒绝整个文件的所有编辑 — 恢复 originalContent
- */
+ /** ★ 拒绝整个文件:先恢复再标记 */
rejectFile(fileId: string): boolean {
const change = this.findChange(fileId);
if (!change) return false;
- this.updateFileStatus(fileId, 'rejected');
-
try {
if (change.type === 'create') {
- // 新建的文件 → 删除
- if (fs.existsSync(change.filePath)) {
- fs.unlinkSync(change.filePath);
- }
+ if (fs.existsSync(change.filePath)) fs.unlinkSync(change.filePath);
} else {
- // 修改的文件 → 恢复原始内容
- this.writeFile(change.filePath, change.originalContent);
+ const ok = this.writeFile(change.filePath, change.originalContent);
+ if (!ok) return false;
}
- console.log(`[AI Diff] 已拒绝: ${change.filePath}`);
- return true;
+ console.log(`[AI Diff] 已拒绝文件: ${change.filePath}`);
} catch (e) {
console.error(`[AI Diff] 恢复文件失败: ${change.filePath}`, e);
return false;
}
+
+ this.updateFileStatus(fileId, 'rejected');
+ return true;
}
- /**
- * 拒绝所有变更
- */
+ /** ★ 拒绝所有 pending 的文件 */
rejectAll(): number {
if (!this.currentSet) return 0;
+ const pendingIds = this.currentSet.changes.filter(c => c.status === 'pending').map(c => c.id);
let count = 0;
- for (const change of this.currentSet.changes) {
- if (change.status === 'pending' && this.rejectFile(change.id)) {
- count++;
- }
+ for (const id of pendingIds) {
+ if (this.rejectFile(id)) count++;
}
return count;
}
- // ---- 工具方法 ----
+ // ---- 工具 ----
- /**
- * 在内容中还原单个编辑
- */
private revertEditInContent(content: string, edit: EditRecord): string {
- // 找到 newString 并替换回 oldString
const idx = content.indexOf(edit.newString);
if (idx !== -1) {
return content.substring(0, idx) + edit.oldString + content.substring(idx + edit.newString.length);
}
- // 找不到精确匹配,回退:使用 beforeContent
console.warn(`[AI Diff] 无法精确定位编辑替换位置,回退到 beforeContent`);
return edit.beforeContent;
}
+ /** ★ 查找下一个待处理的文件变更(用于自动推进) */
+ findNextPending(afterFileId?: string): FileChange | undefined {
+ if (!this.currentSet) return undefined;
+ const pending = this.currentSet.changes.filter(c => c.status === 'pending');
+ if (pending.length === 0) return undefined;
+ if (!afterFileId) return pending[0];
+ const idx = pending.findIndex(c => c.id === afterFileId);
+ if (idx < 0 || idx >= pending.length - 1) {
+ // 当前是最后一个 pending,回到第一个(循环)
+ return pending.length > 1 ? pending[0] : undefined;
+ }
+ return pending[idx + 1];
+ }
+
+ /** 检查文件是否完全处理完毕(所有编辑都已 accepted/rejected) */
+ isFileFullyProcessed(fileId: string): boolean {
+ const change = this.findChange(fileId);
+ if (!change) return true;
+ return change.edits.every(e => e.status !== 'pending');
+ }
+
clear(): void {
this.currentSet = null;
this.originalContents.clear();
@@ -379,7 +329,7 @@ export class ChangeSetManager {
private writeFile(filePath: string, content: string): boolean {
try {
fs.writeFileSync(filePath, content, 'utf-8');
- console.log(`[AI Diff] 写入文件: ${filePath}`);
+ console.log(`[AI Diff] 写入文件: ${filePath} (${content.length} 字符)`);
return true;
} catch (e) {
console.error(`[AI Diff] 写入文件失败: ${filePath}`, e);
@@ -395,10 +345,7 @@ export class ChangeSetManager {
for (const change of changes) {
const changeLines = change.value.split('\n');
- if (changeLines[changeLines.length - 1] === '') {
- changeLines.pop();
- }
-
+ if (changeLines[changeLines.length - 1] === '') changeLines.pop();
for (const line of changeLines) {
if (change.added) {
lines.push({ type: 'add', oldLineNum: 0, newLineNum: newLine, content: line });
@@ -413,18 +360,13 @@ export class ChangeSetManager {
}
}
}
-
return lines;
}
private readFile(filePath: string): string | null {
- try {
- return fs.readFileSync(filePath, 'utf-8');
- } catch {
- return null;
- }
+ try { return fs.readFileSync(filePath, 'utf-8'); } catch { return null; }
}
}
-/** @legacy 向后兼容别名,供未迁移的模块使用 */
+/** @legacy 向后兼容别名 */
export { ChangeSetManager as SnapshotManager };
diff --git a/src/trigger/hookHandler.ts b/src/trigger/hookHandler.ts
index d7e8acf..4fef24f 100644
--- a/src/trigger/hookHandler.ts
+++ b/src/trigger/hookHandler.ts
@@ -1,13 +1,8 @@
/**
* Hook 处理器
*
- * PostToolUse (Edit/Write) → 收集变更
- * + autoShowDiffPerEdit=true → 立即弹出 VSCode 内置 Diff
- * + autoShowDiffPerEdit=false → 静默收集
- *
- * Stop → 对话结束
- * + showAllDiffsOnStop=true → 弹出 QuickPick 文件列表
- * + showAllDiffsOnStop=false → 仅状态栏通知
+ * Edit/Write → recordEdit()
+ * Stop → QuickPick 总览 (含 Accept All / Reject All)
*/
import * as vscode from 'vscode';
@@ -17,131 +12,136 @@ import { DiffViewer } from '../render/diffViewer';
import { ClaudeHookEvent, FileChange } from '../models/types';
import { CONFIG_SECTION, CONFIG_KEYS } from '../models/constants';
-const SETTING_AUTO_SHOW_DIFF = `${CONFIG_SECTION}.${CONFIG_KEYS.AUTO_SHOW_DIFF_PER_EDIT}`;
-const SETTING_SHOW_ALL_ON_STOP = `${CONFIG_SECTION}.${CONFIG_KEYS.SHOW_ALL_DIFFS_ON_STOP}`;
-
export class HookHandler {
- private diffViewer: DiffViewer;
-
constructor(
- private changeSetManager: ChangeSetManager
- ) {
- this.diffViewer = new DiffViewer(changeSetManager);
- }
+ private changeSetManager: ChangeSetManager,
+ private diffViewer: DiffViewer
+ ) {}
getDiffViewer(): DiffViewer {
return this.diffViewer;
}
- /**
- * 处理 PostToolUse 事件(Edit/Write)
- * 收集变更,根据配置决定是否自动弹 Diff
- */
handleEdit(event: ClaudeHookEvent): void {
event.filePath = path.normalize(event.filePath);
console.log(`[AI Diff] 收集变更: ${event.toolName} → ${event.filePath}`);
this.changeSetManager.recordEdit(
- event.filePath,
- event.toolName,
- event.oldString || '',
- event.newString || '',
- event.content || ''
+ event.filePath, event.toolName,
+ event.oldString || '', event.newString || '', event.content || ''
);
- // 根据配置决定是否自动弹 Diff
- const autoShow = vscode.workspace.getConfiguration(CONFIG_SECTION).get(CONFIG_KEYS.AUTO_SHOW_DIFF_PER_EDIT, false);
+ const autoShow = vscode.workspace
+ .getConfiguration(CONFIG_SECTION)
+ .get(CONFIG_KEYS.AUTO_SHOW_DIFF_PER_EDIT, false);
+
if (autoShow) {
const set = this.changeSetManager.getCurrentSet();
if (set) {
- const normalizedPath = path.normalize(event.filePath).replace(/\\/g, '/');
- const fileChange = set.changes.find(
- c => path.normalize(c.filePath).replace(/\\/g, '/') === normalizedPath
- );
- if (fileChange) {
- const lastEdit = fileChange.edits[fileChange.edits.length - 1];
- this.diffViewer.showEditDiff(lastEdit);
- }
+ const norm = path.normalize(event.filePath).replace(/\\/g, '/');
+ const fc = set.changes.find(c => path.normalize(c.filePath).replace(/\\/g, '/') === norm);
+ if (fc) this.diffViewer.showEditDiff(fc.edits[fc.edits.length - 1], fc.id);
}
}
-
- // 更新上下文键
vscode.commands.executeCommand('setContext', 'aiDiffPreview.isActive', true);
}
- /**
- * 处理 Stop 事件(对话结束)
- */
handleStop(): void {
const changeSet = this.changeSetManager.markReady();
- if (!changeSet) {
- console.log('[AI Diff] 对话结束,无文件变更');
- return;
- }
+ if (!changeSet) { console.log('[AI Diff] 对话结束,无文件变更'); return; }
- const count = changeSet.changes.length;
const pendingCount = changeSet.changes.filter(c => c.status === 'pending').length;
- console.log(`[AI Diff] 对话结束,${count} 个文件有变更 (pending: ${pendingCount})`);
+ console.log(`[AI Diff] 对话结束,pending: ${pendingCount}/${changeSet.changes.length}`);
- const showAll = vscode.workspace.getConfiguration(CONFIG_SECTION).get(CONFIG_KEYS.SHOW_ALL_DIFFS_ON_STOP, true);
+ const showAll = vscode.workspace
+ .getConfiguration(CONFIG_SECTION)
+ .get(CONFIG_KEYS.SHOW_ALL_DIFFS_ON_STOP, true);
if (showAll && pendingCount > 0) {
- // 弹出 QuickPick 文件列表
this.showFileListPicker(changeSet.changes.filter(c => c.status === 'pending'));
} else {
- // 仅状态栏通知
const msg = pendingCount > 0
- ? `AI Diff: ${pendingCount} 个文件有待处理变更`
- : `AI Diff: ${count} 个文件变更已完成`;
- vscode.window.showInformationMessage(msg, '查看变更').then(selection => {
- if (selection === '查看变更') {
- this.showFileListPicker(changeSet.changes.filter(c => c.status === 'pending'));
- }
+ ? `AI Diff: ${pendingCount} 个文件有变更`
+ : `AI Diff: ${changeSet.changes.length} 个文件已处理`;
+ vscode.window.showInformationMessage(msg, '查看总览').then(sel => {
+ if (sel === '查看总览') this.showFileListPicker(changeSet.changes.filter(c => c.status === 'pending'));
});
}
}
- /**
- * 弹出 QuickPick 文件列表
- */
async showFileListPicker(fileChanges?: FileChange[]): Promise {
let changes = fileChanges;
if (!changes) {
- const set = this.changeSetManager.getCurrentSet();
- if (!set) return;
- changes = set.changes.filter(c => c.status === 'pending');
+ changes = this.changeSetManager.getCurrentSet()?.changes.filter(c => c.status === 'pending') || [];
}
-
if (changes.length === 0) {
vscode.window.showInformationMessage('AI Diff: 无待处理变更');
return;
}
- const items: vscode.QuickPickItem[] = changes.map(c => {
+ const totalEdits = changes.reduce((s, c) => s + c.edits.filter(e => e.status === 'pending').length, 0);
+ const totalAdds = changes.reduce((s, c) => s + c.diffLines.filter(d => d.type === 'add').length, 0);
+ const totalDels = changes.reduce((s, c) => s + c.diffLines.filter(d => d.type === 'delete').length, 0);
+
+ interface Item extends vscode.QuickPickItem {
+ itemKind: 'file' | 'separator' | 'action';
+ fileChange?: FileChange;
+ action?: 'acceptAll' | 'rejectAll';
+ }
+
+ const items: Item[] = [];
+ const typeMap: Record = { modify: '$(edit)', create: '$(new-file)', delete: '$(trash)' };
+
+ for (const c of changes) {
const name = path.basename(c.filePath);
const adds = c.diffLines.filter(d => d.type === 'add').length;
const dels = c.diffLines.filter(d => d.type === 'delete').length;
- const typeMap: Record = { modify: '$(edit)', create: '$(new-file)', delete: '$(trash)' };
-
- return {
+ items.push({
+ itemKind: 'file',
label: `${typeMap[c.type] || '$(file)'} ${name}`,
description: `+${adds} -${dels} · ${c.edits.length} 次编辑`,
detail: c.filePath,
- // 存储 fileChange id 以便查找
- id: c.id,
- };
+ fileChange: c,
+ });
+ }
+
+ items.push({
+ itemKind: 'separator',
+ label: '──────────────────────────────',
+ description: '',
});
- const selected = await vscode.window.showQuickPick(items, {
- placeHolder: `AI Diff Review — ${changes.length} 个文件有变更`,
- matchOnDescription: true,
- matchOnDetail: true,
+ items.push({
+ itemKind: 'action',
+ label: '$(check-all) ✅ Accept All — 接受所有变更',
+ description: `${changes.length} 文件 · +${totalAdds} -${totalDels} · ${totalEdits} 编辑`,
+ action: 'acceptAll',
});
- if (selected && selected.id) {
- const fileChange = changes.find(c => c.id === selected.id);
- if (fileChange) {
- await this.diffViewer.showFileDiff(fileChange);
+ items.push({
+ itemKind: 'action',
+ label: '$(trash) ❌ Reject All — 拒绝所有变更',
+ description: `${changes.length} 文件 · +${totalAdds} -${totalDels} · ${totalEdits} 编辑`,
+ action: 'rejectAll',
+ });
+
+ const selected = await vscode.window.showQuickPick- (items, {
+ placeHolder: `AI Diff 总览 — ${changes.length} 文件 ${totalEdits} 编辑 (+${totalAdds} -${totalDels})`,
+ matchOnDescription: false,
+ matchOnDetail: false,
+ });
+
+ if (!selected) return;
+
+ if (selected.itemKind === 'file' && selected.fileChange) {
+ await this.diffViewer.showFileDiff(selected.fileChange);
+ } else if (selected.itemKind === 'action') {
+ if (selected.action === 'acceptAll') {
+ const n = this.changeSetManager.acceptAll();
+ vscode.window.showInformationMessage(`AI Diff: 已接受 ${n} 个文件 ✓`);
+ } else if (selected.action === 'rejectAll') {
+ const n = this.changeSetManager.rejectAll();
+ vscode.window.showInformationMessage(`AI Diff: 已拒绝 ${n} 个文件 ✗`);
}
}
}
diff --git a/src/trigger/hookHandler_bak.ts b/src/trigger/hookHandler_bak.ts
new file mode 100644
index 0000000..a51f647
--- /dev/null
+++ b/src/trigger/hookHandler_bak.ts
@@ -0,0 +1,151 @@
+/**
+ * Hook 处理器
+ *
+ * Edit/Write → recordEdit()
+ * Stop → QuickPick 总览 (含 Accept All / Reject All)
+ */
+
+import * as vscode from 'vscode';
+import * as path from 'path';
+import { ChangeSetManager } from '../snapshot/snapshotManager';
+import { DiffViewer } from '../render/diffViewer';
+import { ClaudeHookEvent, FileChange } from '../models/types';
+import { CONFIG_SECTION, CONFIG_KEYS } from '../models/constants';
+
+export class HookHandler {
+ constructor(
+ private changeSetManager: ChangeSetManager,
+ private diffViewer: DiffViewer
+ ) {}
+
+ getDiffViewer(): DiffViewer {
+ return this.diffViewer;
+ }
+
+ handleEdit(event: ClaudeHookEvent): void {
+ event.filePath = path.normalize(event.filePath);
+ console.log(`[AI Diff] 收集变更: ${event.toolName} → ${event.filePath}`);
+
+ this.changeSetManager.recordEdit(
+ event.filePath, event.toolName,
+ event.oldString || '', event.newString || '', event.content || ''
+ );
+
+ const autoShow = vscode.workspace
+ .getConfiguration(CONFIG_SECTION)
+ .get(CONFIG_KEYS.AUTO_SHOW_DIFF_PER_EDIT, false);
+
+ if (autoShow) {
+ const set = this.changeSetManager.getCurrentSet();
+ if (set) {
+ const norm = path.normalize(event.filePath).replace(/\\/g, '/');
+ const fc = set.changes.find(c => path.normalize(c.filePath).replace(/\\/g, '/') === norm);
+ if (fc) this.diffViewer.showEditDiff(fc.edits[fc.edits.length - 1], fc.id);
+ }
+ }
+ vscode.commands.executeCommand('setContext', 'aiDiffPreview.isActive', true);
+ }
+
+ handleStop(): void {
+ const changeSet = this.changeSetManager.markReady();
+ if (!changeSet) { console.log('[AI Diff] 对话结束,无文件变更'); return; }
+
+ const pendingCount = changeSet.changes.filter(c => c.status === 'pending').length;
+ console.log(`[AI Diff] 对话结束,pending: ${pendingCount}/${changeSet.changes.length}`);
+
+ const showAll = vscode.workspace
+ .getConfiguration(CONFIG_SECTION)
+ .get(CONFIG_KEYS.SHOW_ALL_DIFFS_ON_STOP, true);
+
+ if (showAll && pendingCount > 0) {
+ this.showFileListPicker(changeSet.changes.filter(c => c.status === 'pending'));
+ } else {
+ const msg = pendingCount > 0
+ ? `AI Diff: ${pendingCount} 个文件有变更`
+ : `AI Diff: ${changeSet.changes.length} 个文件已处理`;
+ vscode.window.showInformationMessage(msg, '查看总览').then(sel => {
+ if (sel === '查看总览') this.showFileListPicker(changeSet.changes.filter(c => c.status === 'pending'));
+ });
+ }
+ }
+
+ /**
+ * QuickPick 总览: 文件列表 + Accept All / Reject All
+ */
+ async showFileListPicker(fileChanges?: FileChange[]): Promise {
+ let changes = fileChanges;
+ if (!changes) {
+ changes = this.changeSetManager.getCurrentSet()?.changes.filter(c => c.status === 'pending') || [];
+ }
+ if (changes.length === 0) {
+ vscode.window.showInformationMessage('AI Diff: 无待处理变更');
+ return;
+ }
+
+ const totalEdits = changes.reduce((s, c) => s + c.edits.filter(e => e.status === 'pending').length, 0);
+ const totalAdds = changes.reduce((s, c) => s + c.diffLines.filter(d => d.type === 'add').length, 0);
+ const totalDels = changes.reduce((s, c) => s + c.diffLines.filter(d => d.type === 'delete').length, 0);
+
+ interface Item extends vscode.QuickPickItem {
+ itemKind: 'file' | 'separator' | 'action';
+ fileChange?: FileChange;
+ action?: 'acceptAll' | 'rejectAll';
+ }
+
+ const items: Item[] = [];
+ const typeMap: Record = { modify: '$(edit)', create: '$(new-file)', delete: '$(trash)' };
+
+ for (const c of changes) {
+ const name = path.basename(c.filePath);
+ const adds = c.diffLines.filter(d => d.type === 'add').length;
+ const dels = c.diffLines.filter(d => d.type === 'delete').length;
+ items.push({
+ itemKind: 'file',
+ label: `${typeMap[c.type] || '$(file)'} ${name}`,
+ description: `+${adds} -${dels} · ${c.edits.length} 次编辑`,
+ detail: c.filePath,
+ fileChange: c,
+ });
+ }
+
+ items.push({
+ itemKind: 'separator',
+ label: '──────────────────────────────',
+ description: '',
+ });
+
+ items.push({
+ itemKind: 'action',
+ label: '$(check-all) ✅ Accept All — 接受所有变更',
+ description: `${changes.length} 文件 · +${totalAdds} -${totalDels} · ${totalEdits} 编辑`,
+ action: 'acceptAll',
+ });
+
+ items.push({
+ itemKind: 'action',
+ label: '$(trash) ❌ Reject All — 拒绝所有变更',
+ description: `${changes.length} 文件 · +${totalAdds} -${totalDels} · ${totalEdits} 编辑`,
+ action: 'rejectAll',
+ });
+
+ const selected = await vscode.window.showQuickPick
- (items, {
+ placeHolder: `AI Diff 总览 — ${changes.length} 文件 ${totalEdits} 编辑 (+${totalAdds} -${totalDels})`,
+ matchOnDescription: false,
+ matchOnDetail: false,
+ });
+
+ if (!selected) return;
+
+ if (selected.itemKind === 'file' && selected.fileChange) {
+ await this.diffViewer.showFileDiff(selected.fileChange);
+ } else if (selected.itemKind === 'action') {
+ if (selected.action === 'acceptAll') {
+ const n = this.changeSetManager.acceptAll();
+ vscode.window.showInformationMessage(`AI Diff: 已接受 ${n} 个文件 ✓`);
+ } else if (selected.action === 'rejectAll') {
+ const n = this.changeSetManager.rejectAll();
+ vscode.window.showInformationMessage(`AI Diff: 已拒绝 ${n} 个文件 ✗`);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/测试.md b/测试.md
index e7483e2..a04f542 100644
--- a/测试.md
+++ b/测试.md
@@ -52,6 +52,7 @@
## 第五轮测试 - 内嵌悬浮窗验证
这次修改用于验证:
+
1. Webview 面板能否正常弹出
2. 双栏 Diff 是否正确显示新增/删除行
3. Accept 按钮是否生效
@@ -64,3 +65,50 @@
- 修复了所有已知问题
- 现在应该能正常工作
- 验证: 面板弹出 + Accept/Reject + 文件回写
+
+## 第七轮测试
+
+改动内容:
+
+- 语法错误已修复
+- API 调用已优化
+- 新增错误处理逻辑
+
+以下为原始代码(已删除):
+
+```js
+function oldApi() {
+ return fetch('/api').then(r => r.json());
+}
+```
+
+替换为新的 async/await 写法:
+
+```js
+async function newApi() {
+ const res = await fetch('/api');
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ return res.json();
+}
+```
+
+## 第八轮测试
+
+- 代码添加了错误处理
+- 验证 CodeLens 按钮和状态栏是否正常显示
+- 验证 Accept / Reject 快捷键是否生效
+
+## 第九轮测试 - 多文件变更
+
+同时修改了 3 个文件:
+
+1. 测试.md - 本文件
+2. test1.md - 新增功能模块
+3. test2.md - 新增 API 和模块
+
+验证:
+
+- 状态栏显示 "3文件 N编辑"
+- 点击弹出 Webview 总览面板
+- 每个文件可展开查看详细 Diff
+- Accept/Reject 按钮正常生效