mirror of
https://github.com/Gmaker689/ai-diff-preview.git
synced 2026-10-09 06:21:52 +08:00
Compare commits
2 Commits
50b3868cc5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 122c38c51e | |||
| fa44163ca3 |
+26
-3
@@ -1,12 +1,24 @@
|
|||||||
{
|
{
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"PostToolUse": [
|
"PreToolUse": [
|
||||||
{
|
{
|
||||||
"matcher": "Write|Edit",
|
"matcher": "Write",
|
||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const fs=require('fs'),p=require('path'),d=p.join(process.env.CLAUDE_PLUGIN_ROOT||'.','.claude','hooks');let b='';process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(b);const e={type:'edit',filePath:j.tool_input?.file_path||j.tool_response?.filePath||'',content:j.tool_input?.content||'',oldString:j.tool_input?.old_string||'',newString:j.tool_input?.new_string||'',timestamp:Date.now(),toolName:j.tool_name};fs.mkdirSync(d,{recursive:true});const t=p.join(d,'pending.json'),tmp=t+'.tmp';fs.writeFileSync(tmp,JSON.stringify(e));fs.renameSync(tmp,t)}catch{}})\"",
|
"command": "node -e \"const fs=require('fs'),p=require('path');let b='';process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(b);const fp=j.tool_input?.file_path;if(!fp)return;const d=p.join(process.env.CLAUDE_PLUGIN_ROOT||'.','.claude','hooks');fs.mkdirSync(d,{recursive:true});let oc='';try{oc=fs.readFileSync(fp,'utf-8')}catch(e){}const s={originalContent:oc,timestamp:Date.now()};const f=p.join(d,'pre-write-snapshot.json'),tmp=f+'.tmp';fs.writeFileSync(tmp,JSON.stringify(s));fs.renameSync(tmp,f)}catch(e){}})\"",
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Edit",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node -e \"const fs=require('fs'),p=require('path'),d=p.join(process.env.CLAUDE_PLUGIN_ROOT||'.','.claude','hooks');let b='';process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(b);const e={type:'edit',filePath:j.tool_input?.file_path||'',content:j.tool_input?.content||'',oldString:j.tool_input?.old_string||'',newString:j.tool_input?.new_string||'',timestamp:Date.now(),toolName:'Edit'};fs.mkdirSync(d,{recursive:true});const t=p.join(d,'pending.json'),tmp=t+'.tmp';fs.writeFileSync(tmp,JSON.stringify(e));fs.renameSync(tmp,t)}catch{}})\"",
|
||||||
"timeout": 5
|
"timeout": 5
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -22,6 +34,17 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"StopFailure": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node -e \"const fs=require('fs'),p=require('path'),d=p.join(process.env.CLAUDE_PLUGIN_ROOT||'.','.claude','hooks');try{fs.mkdirSync(d,{recursive:true});const t=p.join(d,'pending.json'),tmp=t+'.tmp';fs.writeFileSync(tmp,JSON.stringify({type:'stopFailure',filePath:'',content:'',oldString:'',newString:'',timestamp:Date.now(),toolName:'StopFailure'}));fs.renameSync(tmp,t)}catch{}\"",
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ out/
|
|||||||
*.log
|
*.log
|
||||||
coverage/
|
coverage/
|
||||||
.env
|
.env
|
||||||
|
.claude/hooks/
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Gmaker689
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -22,13 +22,25 @@
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"PostToolUse": [
|
"PreToolUse": [
|
||||||
{
|
{
|
||||||
"matcher": "Write|Edit",
|
"matcher": "Write",
|
||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const fs=require('fs'),p=require('path'),d=p.join(process.env.CLAUDE_PLUGIN_ROOT||'.','.claude','hooks');let b='';process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(b);const e={type:'edit',filePath:j.tool_input?.file_path||j.tool_response?.filePath||'',content:j.tool_input?.content||'',oldString:j.tool_input?.old_string||'',newString:j.tool_input?.new_string||'',timestamp:Date.now(),toolName:j.tool_name};fs.mkdirSync(d,{recursive:true});const t=p.join(d,'pending.json'),tmp=t+'.tmp';fs.writeFileSync(tmp,JSON.stringify(e));fs.renameSync(tmp,t)}catch{}})\"",
|
"command": "node -e \"const fs=require('fs'),p=require('path');let b='';process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(b);const fp=j.tool_input?.file_path;if(!fp)return;const d=p.join(process.env.CLAUDE_PLUGIN_ROOT||'.','.claude','hooks');fs.mkdirSync(d,{recursive:true});let oc='';try{oc=fs.readFileSync(fp,'utf-8')}catch(e){}const s={originalContent:oc,timestamp:Date.now()};const f=p.join(d,'pre-write-snapshot.json'),tmp=f+'.tmp';fs.writeFileSync(tmp,JSON.stringify(s));fs.renameSync(tmp,f)}catch(e){}})\"",
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Edit",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node -e \"const fs=require('fs'),p=require('path'),d=p.join(process.env.CLAUDE_PLUGIN_ROOT||'.','.claude','hooks');let b='';process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(b);const e={type:'edit',filePath:j.tool_input?.file_path||'',content:j.tool_input?.content||'',oldString:j.tool_input?.old_string||'',newString:j.tool_input?.new_string||'',timestamp:Date.now(),toolName:'Edit'};fs.mkdirSync(d,{recursive:true});const t=p.join(d,'pending.json'),tmp=t+'.tmp';fs.writeFileSync(tmp,JSON.stringify(e));fs.renameSync(tmp,t)}catch{}})\"",
|
||||||
"timeout": 5
|
"timeout": 5
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -44,20 +56,40 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"StopFailure": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node -e \"const fs=require('fs'),p=require('path'),d=p.join(process.env.CLAUDE_PLUGIN_ROOT||'.','.claude','hooks');try{fs.mkdirSync(d,{recursive:true});const t=p.join(d,'pending.json'),tmp=t+'.tmp';fs.writeFileSync(tmp,JSON.stringify({type:'stopFailure',filePath:'',content:'',oldString:'',newString:'',timestamp:Date.now(),toolName:'StopFailure'}));fs.renameSync(tmp,t)}catch{}\"",
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Hooks 说明
|
||||||
|
|
||||||
|
| Hook | 匹配器 | 作用 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **PreToolUse** | `Write` | 写入前保存文件原始内容快照,用于 diff 对比 |
|
||||||
|
| **PostToolUse** | `Edit` | 记录编辑信息(oldString/newString),用于精确 diff |
|
||||||
|
| **Stop** | — | Claude 正常完成响应 → 触发变更总览 |
|
||||||
|
| **StopFailure** | — | API 错误导致中断 → 触发变更总览 |
|
||||||
|
|
||||||
## 命令
|
## 命令
|
||||||
|
|
||||||
| 命令 | 快捷键 | 功能 |
|
| 命令 | 快捷键 | 功能 |
|
||||||
| --------------------------------- | ---------------- | ---------------------------------------- |
|
| --- | --- | --- |
|
||||||
| `AI Diff: 显示所有变更文件列表` | `Ctrl+Shift+D` | QuickPick 总览 + Accept All / Reject All |
|
| `AI Diff: 显示所有变更文件列表` | `Ctrl+Shift+D` | QuickPick 总览 + Accept All / Reject All |
|
||||||
| `AI Diff: 下一个变更` | `Alt+↓` | 跳转下一个 pending 文件 |
|
| `AI Diff: 下一个变更` | `Alt+↓` | 跳转下一个 pending 文件 |
|
||||||
| `AI Diff: 上一个变更` | `Alt+↑` | 跳转上一个 pending 文件 |
|
| `AI Diff: 上一个变更` | `Alt+↑` | 跳转上一个 pending 文件 |
|
||||||
| `AI Diff: 接受所有变更` | `Ctrl+Shift+A` | Accept All |
|
| `AI Diff: 接受所有变更` | `Ctrl+Shift+A` | Accept All |
|
||||||
| `AI Diff: 拒绝所有变更` | `Ctrl+Shift+R` | Reject All |
|
| `AI Diff: 拒绝所有变更` | `Ctrl+Shift+R` | Reject All |
|
||||||
|
|
||||||
Diff 编辑器打开后,状态栏右侧自动出现 **Accept / Reject / 接受全部** 按钮。
|
Diff 编辑器打开后,状态栏右侧自动出现 **Accept / Reject / 接受全部** 按钮。
|
||||||
|
|
||||||
|
|||||||
Generated
+24
-7
@@ -1,17 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "ai-diff-preview",
|
"name": "ai-diff-preview",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ai-diff-preview",
|
"name": "ai-diff-preview",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff": "^5.0.0",
|
"diff": "^5.0.0",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@esbuild/win32-x64": "^0.28.1",
|
||||||
"@types/diff": "^5.0.0",
|
"@types/diff": "^5.0.0",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@types/uuid": "^9.0.0",
|
"@types/uuid": "^9.0.0",
|
||||||
@@ -404,20 +405,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.19.12",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||||
"integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
|
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
@@ -1790,6 +1790,23 @@
|
|||||||
"@esbuild/win32-x64": "0.19.12"
|
"@esbuild/win32-x64": "0.19.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/esbuild/node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.19.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
|
||||||
|
"integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escape-string-regexp": {
|
"node_modules/escape-string-regexp": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
|
|||||||
+39
-2
@@ -16,7 +16,9 @@
|
|||||||
"Programming Languages",
|
"Programming Languages",
|
||||||
"Other"
|
"Other"
|
||||||
],
|
],
|
||||||
"activationEvents": ["onStartupFinished"],
|
"activationEvents": [
|
||||||
|
"onStartupFinished"
|
||||||
|
],
|
||||||
"main": "./dist/extension.js",
|
"main": "./dist/extension.js",
|
||||||
"contributes": {
|
"contributes": {
|
||||||
"commands": [
|
"commands": [
|
||||||
@@ -57,6 +59,18 @@
|
|||||||
{
|
{
|
||||||
"command": "aiDiffPreview.prevDiff",
|
"command": "aiDiffPreview.prevDiff",
|
||||||
"title": "AI Diff: 上一个变更"
|
"title": "AI Diff: 上一个变更"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "aiDiffPreview.nextEdit",
|
||||||
|
"title": "AI Diff: 下一次编辑"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "aiDiffPreview.prevEdit",
|
||||||
|
"title": "AI Diff: 上一次编辑"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "aiDiffPreview.showEditAtIndex",
|
||||||
|
"title": "AI Diff: 逐次查看编辑"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"keybindings": [
|
"keybindings": [
|
||||||
@@ -105,6 +119,18 @@
|
|||||||
"key": "ctrl+shift+f",
|
"key": "ctrl+shift+f",
|
||||||
"mac": "cmd+shift+f",
|
"mac": "cmd+shift+f",
|
||||||
"when": "aiDiffPreview.isActive"
|
"when": "aiDiffPreview.isActive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "aiDiffPreview.nextEdit",
|
||||||
|
"key": "alt+right",
|
||||||
|
"mac": "alt+right",
|
||||||
|
"when": "aiDiffPreview.isActive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "aiDiffPreview.prevEdit",
|
||||||
|
"key": "alt+left",
|
||||||
|
"mac": "alt+left",
|
||||||
|
"when": "aiDiffPreview.isActive"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -128,7 +154,12 @@
|
|||||||
"aiDiffPreview.floatingLabelMode": {
|
"aiDiffPreview.floatingLabelMode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "statusBar",
|
"default": "statusBar",
|
||||||
"enum": ["inline", "statusBar", "both", "none"],
|
"enum": [
|
||||||
|
"inline",
|
||||||
|
"statusBar",
|
||||||
|
"both",
|
||||||
|
"none"
|
||||||
|
],
|
||||||
"enumDescriptions": [
|
"enumDescriptions": [
|
||||||
"编辑器行内 Accept/Reject 标签(通过 CodeLens 显示)",
|
"编辑器行内 Accept/Reject 标签(通过 CodeLens 显示)",
|
||||||
"状态栏底部固定 Accept/Reject 按钮",
|
"状态栏底部固定 Accept/Reject 按钮",
|
||||||
@@ -141,6 +172,11 @@
|
|||||||
"type": "number",
|
"type": "number",
|
||||||
"default": 100000,
|
"default": 100000,
|
||||||
"description": "最大处理文件大小(字符数)"
|
"description": "最大处理文件大小(字符数)"
|
||||||
|
},
|
||||||
|
"aiDiffPreview.idleTimeout": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 3000,
|
||||||
|
"description": "空闲检测超时时间(毫秒)。在此时间内无新编辑事件,自动触发 Diff 提示(用于 ESC 打断场景)。设为 0 则禁用此功能"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,6 +191,7 @@
|
|||||||
"package": "vsce package"
|
"package": "vsce package"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@esbuild/win32-x64": "^0.28.1",
|
||||||
"@types/diff": "^5.0.0",
|
"@types/diff": "^5.0.0",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@types/uuid": "^9.0.0",
|
"@types/uuid": "^9.0.0",
|
||||||
|
|||||||
+44
-10
@@ -30,6 +30,10 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
hookHandler = new HookHandler(changeSetManager, diffViewer);
|
hookHandler = new HookHandler(changeSetManager, diffViewer);
|
||||||
statusBar = new StatusBarManager(changeSetManager, diffViewer);
|
statusBar = new StatusBarManager(changeSetManager, diffViewer);
|
||||||
|
|
||||||
|
// ★ 设置 hook 目录路径,供 snapshotManager 读取 PreToolUse 快照
|
||||||
|
const hookDir = getHookDir();
|
||||||
|
if (hookDir) changeSetManager.setHookDir(hookDir);
|
||||||
|
|
||||||
// DiffViewer 操作后刷新状态栏
|
// DiffViewer 操作后刷新状态栏
|
||||||
diffViewer.onDiffAction = () => refreshUI();
|
diffViewer.onDiffAction = () => refreshUI();
|
||||||
|
|
||||||
@@ -95,6 +99,24 @@ function registerCommands(context: vscode.ExtensionContext) {
|
|||||||
vscode.commands.registerCommand(COMMANDS.PREV_DIFF, () => navigateDiff(-1))
|
vscode.commands.registerCommand(COMMANDS.PREV_DIFF, () => navigateDiff(-1))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ★ 逐次编辑导航
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('aiDiffPreview.nextEdit', () => diffViewer.showNextEdit())
|
||||||
|
);
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('aiDiffPreview.prevEdit', () => diffViewer.showPrevEdit())
|
||||||
|
);
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('aiDiffPreview.showEditAtIndex', () => {
|
||||||
|
const session = diffViewer.getActiveSession();
|
||||||
|
if (session) {
|
||||||
|
const set = changeSetManager.getCurrentSet();
|
||||||
|
const fc = set?.changes.find(c => c.id === session.fileId);
|
||||||
|
if (fc) diffViewer.showEditAtIndex(fc, 0);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Diff 界面状态栏按钮
|
// Diff 界面状态栏按钮
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand('aiDiffPreview.acceptCurrentDiff', async () => {
|
vscode.commands.registerCommand('aiDiffPreview.acceptCurrentDiff', async () => {
|
||||||
@@ -150,6 +172,14 @@ function refreshUI(): void {
|
|||||||
vscode.commands.executeCommand('setContext', 'aiDiffPreview.isActive', hasPending);
|
vscode.commands.executeCommand('setContext', 'aiDiffPreview.isActive', hasPending);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Hook 目录 ----
|
||||||
|
|
||||||
|
function getHookDir(): string | null {
|
||||||
|
const folder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (!folder) return null;
|
||||||
|
return path.join(folder.uri.fsPath, '.claude', 'hooks');
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Hook 监听 ----
|
// ---- Hook 监听 ----
|
||||||
|
|
||||||
function registerHookFileWatcher(context: vscode.ExtensionContext) {
|
function registerHookFileWatcher(context: vscode.ExtensionContext) {
|
||||||
@@ -160,11 +190,13 @@ function registerHookFileWatcher(context: vscode.ExtensionContext) {
|
|||||||
if (!fs.existsSync(hookDir)) fs.mkdirSync(hookDir, { recursive: true });
|
if (!fs.existsSync(hookDir)) fs.mkdirSync(hookDir, { recursive: true });
|
||||||
|
|
||||||
const pendingFile = path.join(hookDir, 'pending.json');
|
const pendingFile = path.join(hookDir, 'pending.json');
|
||||||
const watcher = vscode.workspace.createFileSystemWatcher(
|
|
||||||
|
// ★ 监听 pending.json(Edit/Stop/StopFailure 事件)
|
||||||
|
const pendingWatcher = vscode.workspace.createFileSystemWatcher(
|
||||||
new vscode.RelativePattern(hookDir, 'pending.json')
|
new vscode.RelativePattern(hookDir, 'pending.json')
|
||||||
);
|
);
|
||||||
|
|
||||||
const handle = () => {
|
const handlePending = () => {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(pendingFile)) return;
|
if (!fs.existsSync(pendingFile)) return;
|
||||||
const content = fs.readFileSync(pendingFile, 'utf-8');
|
const content = fs.readFileSync(pendingFile, 'utf-8');
|
||||||
@@ -172,18 +204,20 @@ function registerHookFileWatcher(context: vscode.ExtensionContext) {
|
|||||||
const event: ClaudeHookEvent = JSON.parse(content);
|
const event: ClaudeHookEvent = JSON.parse(content);
|
||||||
if (Date.now() - event.timestamp > 10000) return;
|
if (Date.now() - event.timestamp > 10000) return;
|
||||||
|
|
||||||
if (event.toolName === 'Stop') {
|
if (event.toolName === 'Stop' || event.toolName === 'StopFailure') {
|
||||||
|
// ★ 对话结束(正常完成或 API 失败)→ 触发 diff 总览
|
||||||
hookHandler.handleStop();
|
hookHandler.handleStop();
|
||||||
} else if (event.toolName === 'Edit' || event.toolName === 'Write') {
|
} else if (event.toolName === 'Edit') {
|
||||||
hookHandler.handleEdit(event);
|
hookHandler.handleEdit(event);
|
||||||
}
|
}
|
||||||
refreshUI();
|
refreshUI();
|
||||||
} catch (e) { console.error('[AI Diff] 处理触发文件失败:', e); }
|
} catch (e) { console.error('[AI Diff] 处理 pending 文件失败:', e); }
|
||||||
};
|
};
|
||||||
|
|
||||||
watcher.onDidChange(handle);
|
pendingWatcher.onDidChange(handlePending);
|
||||||
watcher.onDidCreate(handle);
|
pendingWatcher.onDidCreate(handlePending);
|
||||||
context.subscriptions.push(watcher);
|
context.subscriptions.push(pendingWatcher);
|
||||||
|
|
||||||
console.log(`[AI Diff] Hook 监听已启动: ${hookDir}`);
|
console.log(`[AI Diff] Hook 监听已启动: ${hookDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,8 +233,8 @@ function checkPendingHookFile() {
|
|||||||
fs.unlinkSync(pendingFile);
|
fs.unlinkSync(pendingFile);
|
||||||
const event: ClaudeHookEvent = JSON.parse(content);
|
const event: ClaudeHookEvent = JSON.parse(content);
|
||||||
if (Date.now() - event.timestamp < 15000) {
|
if (Date.now() - event.timestamp < 15000) {
|
||||||
if (event.toolName === 'Stop') hookHandler.handleStop();
|
if (event.toolName === 'Stop' || event.toolName === 'StopFailure') hookHandler.handleStop();
|
||||||
else if (event.toolName === 'Edit' || event.toolName === 'Write') hookHandler.handleEdit(event);
|
else if (event.toolName === 'Edit') hookHandler.handleEdit(event);
|
||||||
refreshUI();
|
refreshUI();
|
||||||
}
|
}
|
||||||
} catch (e) { console.error('[AI Diff] 处理 pending hook 失败:', e); }
|
} catch (e) { console.error('[AI Diff] 处理 pending hook 失败:', e); }
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ export const COMMANDS = {
|
|||||||
NEXT_DIFF: 'aiDiffPreview.nextDiff',
|
NEXT_DIFF: 'aiDiffPreview.nextDiff',
|
||||||
/** 跳转到上一个待处理变更 */
|
/** 跳转到上一个待处理变更 */
|
||||||
PREV_DIFF: 'aiDiffPreview.prevDiff',
|
PREV_DIFF: 'aiDiffPreview.prevDiff',
|
||||||
|
/** ★ 显示下一次编辑 */
|
||||||
|
NEXT_EDIT: 'aiDiffPreview.nextEdit',
|
||||||
|
/** ★ 显示上一次编辑 */
|
||||||
|
PREV_EDIT: 'aiDiffPreview.prevEdit',
|
||||||
|
/** ★ 切换到逐次编辑查看模式 */
|
||||||
|
SHOW_EDIT_AT_INDEX: 'aiDiffPreview.showEditAtIndex',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,6 +52,8 @@ export const CONFIG_KEYS = {
|
|||||||
SHOW_ALL_DIFFS_ON_STOP: 'showAllDiffsOnStop',
|
SHOW_ALL_DIFFS_ON_STOP: 'showAllDiffsOnStop',
|
||||||
FLOATING_LABEL_MODE: 'floatingLabelMode',
|
FLOATING_LABEL_MODE: 'floatingLabelMode',
|
||||||
MAX_FILE_SIZE: 'maxFileSize',
|
MAX_FILE_SIZE: 'maxFileSize',
|
||||||
|
/** ★ 空闲检测超时时间(毫秒),ESC 打断后自动触发 diff */
|
||||||
|
IDLE_TIMEOUT: 'idleTimeout',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ import { ChangeSetManager } from '../snapshot/snapshotManager';
|
|||||||
export interface DiffSession {
|
export interface DiffSession {
|
||||||
fileId: string;
|
fileId: string;
|
||||||
editId?: string;
|
editId?: string;
|
||||||
|
/** ★ 当前查看的编辑索引(undefined 表示聚合视图) */
|
||||||
|
editIndex?: number;
|
||||||
|
/** ★ 当前文件的总编辑数 */
|
||||||
|
totalEdits?: number;
|
||||||
beforeTmpPath: string;
|
beforeTmpPath: string;
|
||||||
afterTmpPath: string;
|
afterTmpPath: string;
|
||||||
beforeUri: vscode.Uri;
|
beforeUri: vscode.Uri;
|
||||||
@@ -65,7 +69,7 @@ export class DiffViewer {
|
|||||||
'vscode.diff',
|
'vscode.diff',
|
||||||
this.activeSession!.beforeUri,
|
this.activeSession!.beforeUri,
|
||||||
this.activeSession!.afterUri,
|
this.activeSession!.afterUri,
|
||||||
`${fileName} — AI ${toolLabel} #${editRecord.id.slice(0, 6)} · 状态栏操作`
|
`${fileName} — AI ${toolLabel} · 编辑 #${editRecord.id.slice(0, 6)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.isOpening = false;
|
this.isOpening = false;
|
||||||
@@ -92,7 +96,7 @@ export class DiffViewer {
|
|||||||
'vscode.diff',
|
'vscode.diff',
|
||||||
this.activeSession!.beforeUri,
|
this.activeSession!.beforeUri,
|
||||||
this.activeSession!.afterUri,
|
this.activeSession!.afterUri,
|
||||||
`${fileName} — ${fileChange.edits.length} 次 AI 编辑 · 状态栏操作`
|
`${fileName} — 聚合视图 (${fileChange.edits.length} 次编辑)`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.isOpening = false;
|
this.isOpening = false;
|
||||||
@@ -112,6 +116,63 @@ export class DiffViewer {
|
|||||||
else vscode.window.showInformationMessage('AI Diff: 当前文件无待处理变更');
|
else vscode.window.showInformationMessage('AI Diff: 当前文件无待处理变更');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- ★ 逐次编辑导航 ----
|
||||||
|
|
||||||
|
/** 显示文件的第 N 次编辑的 diff */
|
||||||
|
async showEditAtIndex(fileChange: FileChange, editIndex: number): Promise<void> {
|
||||||
|
if (editIndex < 0 || editIndex >= fileChange.edits.length) return;
|
||||||
|
const edit = fileChange.edits[editIndex];
|
||||||
|
await this.showEditDiff(edit, fileChange.id);
|
||||||
|
// ★ 更新 session 的编辑索引信息
|
||||||
|
if (this.activeSession) {
|
||||||
|
this.activeSession.editIndex = editIndex;
|
||||||
|
this.activeSession.totalEdits = fileChange.edits.length;
|
||||||
|
}
|
||||||
|
this.onDiffAction?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示下一次编辑 */
|
||||||
|
async showNextEdit(): Promise<void> {
|
||||||
|
if (!this.activeSession) return;
|
||||||
|
const { fileId, editIndex } = this.activeSession;
|
||||||
|
if (editIndex === undefined) return;
|
||||||
|
const fc = this.findFileChange(fileId);
|
||||||
|
if (!fc) return;
|
||||||
|
const nextIdx = editIndex + 1;
|
||||||
|
if (nextIdx < fc.edits.length) {
|
||||||
|
await this.showEditAtIndex(fc, nextIdx);
|
||||||
|
} else {
|
||||||
|
vscode.window.showInformationMessage(`AI Diff: 已是最后一次编辑 (${fc.edits.length}/${fc.edits.length})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示上一次编辑 */
|
||||||
|
async showPrevEdit(): Promise<void> {
|
||||||
|
if (!this.activeSession) return;
|
||||||
|
const { fileId, editIndex } = this.activeSession;
|
||||||
|
if (editIndex === undefined) return;
|
||||||
|
const fc = this.findFileChange(fileId);
|
||||||
|
if (!fc) return;
|
||||||
|
const prevIdx = editIndex - 1;
|
||||||
|
if (prevIdx >= 0) {
|
||||||
|
await this.showEditAtIndex(fc, prevIdx);
|
||||||
|
} else {
|
||||||
|
vscode.window.showInformationMessage('AI Diff: 已是第一次编辑 (1)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换到聚合视图(查看文件整体 diff) */
|
||||||
|
async showAggregatedDiff(): Promise<void> {
|
||||||
|
if (!this.activeSession) return;
|
||||||
|
const fc = this.findFileChange(this.activeSession.fileId);
|
||||||
|
if (fc) await this.showFileDiff(fc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private findFileChange(fileId: string): FileChange | undefined {
|
||||||
|
const set = this.changeSetManager.getCurrentSet();
|
||||||
|
return set?.changes.find(c => c.id === fileId);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Accept/Reject(Accept/Reject 后删除临时文件 + 关闭 Diff) ----
|
// ---- Accept/Reject(Accept/Reject 后删除临时文件 + 关闭 Diff) ----
|
||||||
|
|
||||||
async acceptCurrentDiff(): Promise<void> {
|
async acceptCurrentDiff(): Promise<void> {
|
||||||
|
|||||||
+40
-1
@@ -19,6 +19,10 @@ export class StatusBarManager {
|
|||||||
private acceptBtn: vscode.StatusBarItem;
|
private acceptBtn: vscode.StatusBarItem;
|
||||||
/** Diff 模式 Reject 按钮(拒绝当前文件所有变更) */
|
/** Diff 模式 Reject 按钮(拒绝当前文件所有变更) */
|
||||||
private rejectBtn: vscode.StatusBarItem;
|
private rejectBtn: vscode.StatusBarItem;
|
||||||
|
/** ★ 上一次编辑按钮 */
|
||||||
|
private prevEditBtn: vscode.StatusBarItem;
|
||||||
|
/** ★ 下一次编辑按钮 */
|
||||||
|
private nextEditBtn: vscode.StatusBarItem;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private changeSetManager: ChangeSetManager,
|
private changeSetManager: ChangeSetManager,
|
||||||
@@ -38,9 +42,19 @@ export class StatusBarManager {
|
|||||||
// Reject 按钮(拒绝当前文件所有变更)
|
// Reject 按钮(拒绝当前文件所有变更)
|
||||||
this.rejectBtn = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 99);
|
this.rejectBtn = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 99);
|
||||||
this.rejectBtn.command = 'aiDiffPreview.rejectCurrentDiff';
|
this.rejectBtn.command = 'aiDiffPreview.rejectCurrentDiff';
|
||||||
|
|
||||||
|
// ★ 上一次编辑按钮
|
||||||
|
this.prevEditBtn = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 98);
|
||||||
|
this.prevEditBtn.command = 'aiDiffPreview.prevEdit';
|
||||||
|
this.prevEditBtn.tooltip = '上一次编辑 (Alt+Left)';
|
||||||
|
|
||||||
|
// ★ 下一次编辑按钮
|
||||||
|
this.nextEditBtn = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 97);
|
||||||
|
this.nextEditBtn.command = 'aiDiffPreview.nextEdit';
|
||||||
|
this.nextEditBtn.tooltip = '下一次编辑 (Alt+Right)';
|
||||||
}
|
}
|
||||||
|
|
||||||
enterDiffMode(_session: DiffSession): void {
|
enterDiffMode(session: DiffSession): void {
|
||||||
this.acceptBtn.text = '$(check) Accept';
|
this.acceptBtn.text = '$(check) Accept';
|
||||||
this.acceptBtn.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
|
this.acceptBtn.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
|
||||||
this.acceptBtn.tooltip = '接受此文件所有变更';
|
this.acceptBtn.tooltip = '接受此文件所有变更';
|
||||||
@@ -50,11 +64,34 @@ export class StatusBarManager {
|
|||||||
this.rejectBtn.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
|
this.rejectBtn.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
|
||||||
this.rejectBtn.tooltip = '拒绝此文件所有变更';
|
this.rejectBtn.tooltip = '拒绝此文件所有变更';
|
||||||
this.rejectBtn.show();
|
this.rejectBtn.show();
|
||||||
|
|
||||||
|
// ★ 根据 session 状态显示编辑导航按钮
|
||||||
|
if (session.editIndex !== undefined && session.totalEdits !== undefined) {
|
||||||
|
// 逐次编辑模式:显示 Prev/Next 和计数
|
||||||
|
this.prevEditBtn.text = `$(arrow-left) ${session.editIndex + 1}/${session.totalEdits}`;
|
||||||
|
this.prevEditBtn.command = 'aiDiffPreview.prevEdit';
|
||||||
|
this.prevEditBtn.tooltip = `上一次编辑 (Alt+Left) — 当前第 ${session.editIndex + 1}/${session.totalEdits} 次`;
|
||||||
|
this.prevEditBtn.show();
|
||||||
|
|
||||||
|
this.nextEditBtn.text = '$(arrow-right)';
|
||||||
|
this.nextEditBtn.command = 'aiDiffPreview.nextEdit';
|
||||||
|
this.nextEditBtn.tooltip = '下一次编辑 (Alt+Right)';
|
||||||
|
this.nextEditBtn.show();
|
||||||
|
} else {
|
||||||
|
// 聚合视图模式:显示"逐次查看"入口
|
||||||
|
this.prevEditBtn.text = '$(list-ordered) 逐次查看';
|
||||||
|
this.prevEditBtn.command = 'aiDiffPreview.showEditAtIndex';
|
||||||
|
this.prevEditBtn.tooltip = '切换到逐次编辑查看模式';
|
||||||
|
this.prevEditBtn.show();
|
||||||
|
this.nextEditBtn.hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitDiffMode(): void {
|
exitDiffMode(): void {
|
||||||
this.acceptBtn.hide();
|
this.acceptBtn.hide();
|
||||||
this.rejectBtn.hide();
|
this.rejectBtn.hide();
|
||||||
|
this.prevEditBtn.hide();
|
||||||
|
this.nextEditBtn.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ★ 常驻刷新 */
|
/** ★ 常驻刷新 */
|
||||||
@@ -98,5 +135,7 @@ export class StatusBarManager {
|
|||||||
this.summaryItem.dispose();
|
this.summaryItem.dispose();
|
||||||
this.acceptBtn.dispose();
|
this.acceptBtn.dispose();
|
||||||
this.rejectBtn.dispose();
|
this.rejectBtn.dispose();
|
||||||
|
this.prevEditBtn.dispose();
|
||||||
|
this.nextEditBtn.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,13 @@ function norm(p: string): string {
|
|||||||
export class ChangeSetManager {
|
export class ChangeSetManager {
|
||||||
private currentSet: ChangeSet | null = null;
|
private currentSet: ChangeSet | null = null;
|
||||||
private originalContents: Map<string, string> = new Map();
|
private originalContents: Map<string, string> = new Map();
|
||||||
|
/** ★ .claude/hooks 目录路径,用于读取 PreToolUse 快照 */
|
||||||
|
private hookDir: string | null = null;
|
||||||
|
|
||||||
|
/** 设置 hook 目录路径(由 extension.ts 在启动时调用) */
|
||||||
|
setHookDir(dir: string): void {
|
||||||
|
this.hookDir = dir;
|
||||||
|
}
|
||||||
|
|
||||||
startNewSet(): string {
|
startNewSet(): string {
|
||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
@@ -128,7 +135,8 @@ export class ChangeSetManager {
|
|||||||
}
|
}
|
||||||
let original: string;
|
let original: string;
|
||||||
if (toolName === 'Write') {
|
if (toolName === 'Write') {
|
||||||
original = '';
|
// ★ 读取 PreToolUse 保存的原始文件内容
|
||||||
|
original = this.readPreWriteSnapshot();
|
||||||
} else if (toolName === 'Edit' && oldString) {
|
} else if (toolName === 'Edit' && oldString) {
|
||||||
const idx = currentContent.indexOf(newString);
|
const idx = currentContent.indexOf(newString);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
@@ -144,6 +152,26 @@ export class ChangeSetManager {
|
|||||||
return original;
|
return original;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ★ 读取 PreToolUse hook 保存的文件原始内容快照
|
||||||
|
* PreToolUse 在 Write 执行前触发,会将文件当前内容保存到 pre-write-snapshot.json
|
||||||
|
*/
|
||||||
|
private readPreWriteSnapshot(): string {
|
||||||
|
try {
|
||||||
|
if (!this.hookDir) return '';
|
||||||
|
const snapshotPath = path.join(this.hookDir, 'pre-write-snapshot.json');
|
||||||
|
if (!fs.existsSync(snapshotPath)) return '';
|
||||||
|
const data = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8'));
|
||||||
|
// 读完即删,避免下次误用
|
||||||
|
try { fs.unlinkSync(snapshotPath); } catch { /* ok */ }
|
||||||
|
const content = data.originalContent || '';
|
||||||
|
console.log(`[AI Diff] 读取 PreToolUse 快照: (${content.length} 字符)`);
|
||||||
|
return content;
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private reverseEdit(currentContent: string, oldString: string, newString: string): string {
|
private reverseEdit(currentContent: string, oldString: string, newString: string): string {
|
||||||
if (!newString) return currentContent;
|
if (!newString) return currentContent;
|
||||||
const idx = currentContent.indexOf(newString);
|
const idx = currentContent.indexOf(newString);
|
||||||
@@ -313,6 +341,16 @@ export class ChangeSetManager {
|
|||||||
clear(): void {
|
clear(): void {
|
||||||
this.currentSet = null;
|
this.currentSet = null;
|
||||||
this.originalContents.clear();
|
this.originalContents.clear();
|
||||||
|
this.cleanupPreWriteSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清理可能残留的 PreToolUse 快照文件 */
|
||||||
|
private cleanupPreWriteSnapshot(): void {
|
||||||
|
try {
|
||||||
|
if (!this.hookDir) return;
|
||||||
|
const snapshotPath = path.join(this.hookDir, 'pre-write-snapshot.json');
|
||||||
|
if (fs.existsSync(snapshotPath)) fs.unlinkSync(snapshotPath);
|
||||||
|
} catch { /* ok */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
hasPendingChanges(): boolean {
|
hasPendingChanges(): boolean {
|
||||||
|
|||||||
@@ -134,7 +134,29 @@ export class HookHandler {
|
|||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
|
|
||||||
if (selected.itemKind === 'file' && selected.fileChange) {
|
if (selected.itemKind === 'file' && selected.fileChange) {
|
||||||
await this.diffViewer.showFileDiff(selected.fileChange);
|
if (selected.fileChange.edits.length > 1) {
|
||||||
|
// ★ 多次编辑:弹出二级选择
|
||||||
|
const choice = await vscode.window.showQuickPick([
|
||||||
|
{
|
||||||
|
label: '$(diff) 聚合视图',
|
||||||
|
description: `查看 ${selected.fileChange.edits.length} 次编辑的总 diff`,
|
||||||
|
mode: 'aggregate' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '$(list-ordered) 逐次查看',
|
||||||
|
description: '逐个查看每次编辑的 diff,支持 Accept/Reject 单次编辑',
|
||||||
|
mode: 'perEdit' as const,
|
||||||
|
},
|
||||||
|
], { placeHolder: `${path.basename(selected.fileChange.filePath)} — 选择查看模式` });
|
||||||
|
if (!choice) return;
|
||||||
|
if (choice.mode === 'perEdit') {
|
||||||
|
await this.diffViewer.showEditAtIndex(selected.fileChange, 0);
|
||||||
|
} else {
|
||||||
|
await this.diffViewer.showFileDiff(selected.fileChange);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.diffViewer.showFileDiff(selected.fileChange);
|
||||||
|
}
|
||||||
} else if (selected.itemKind === 'action') {
|
} else if (selected.itemKind === 'action') {
|
||||||
if (selected.action === 'acceptAll') {
|
if (selected.action === 'acceptAll') {
|
||||||
const n = this.changeSetManager.acceptAll();
|
const n = this.changeSetManager.acceptAll();
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
# AI Diff Preview 测试文件
|
|
||||||
|
|
||||||
这是一个测试文件,用于验证 AI Diff Preview 插件的完整工作流程。
|
|
||||||
|
|
||||||
## 已完成功能
|
|
||||||
|
|
||||||
- ✅ 快照管理(Base/Suggest 双版本隔离)
|
|
||||||
- ✅ Diff 计算引擎(Myers 算法 + 变更块切割)
|
|
||||||
- ✅ Accept/Reject 事务处理(原子化写入)
|
|
||||||
- ✅ Claude Hooks 集成(PostToolUse 触发)
|
|
||||||
- ✅ 内联装饰器渲染(红绿高亮)
|
|
||||||
- ✅ CodeLens 按钮(逐块操作)
|
|
||||||
|
|
||||||
## 待实现功能
|
|
||||||
|
|
||||||
- Diff 面板(多文件对比视图)
|
|
||||||
- 冲突检测(用户手动修改检测)
|
|
||||||
- 状态栏集成
|
|
||||||
- 快捷键绑定
|
|
||||||
|
|
||||||
## 测试说明
|
|
||||||
|
|
||||||
修改此文件后,PostToolUse hook 会触发 VSCode 扩展,
|
|
||||||
自动创建快照并显示 Diff 预览,可通过 Accept/Reject 操作变更。
|
|
||||||
|
|
||||||
## 测试记录
|
|
||||||
|
|
||||||
- 2026-06-17: 第一次测试 hook 触发链路
|
|
||||||
- 2026-06-17: 第二次测试 - 改为原生 Diff Editor 方案
|
|
||||||
- 预期效果: 对话结束后弹出通知,点击后打开 VSCode 原生双栏 Diff
|
|
||||||
- 状态: 重启后测试
|
|
||||||
|
|
||||||
## 新增测试内容
|
|
||||||
|
|
||||||
这段文字用于验证 PostToolUse hook 能否正确收集变更,
|
|
||||||
并在对话结束后通过 VSCode 原生 Diff Editor 展示。
|
|
||||||
|
|
||||||
## 第三轮测试
|
|
||||||
|
|
||||||
- 改为内嵌 Webview 悬浮窗
|
|
||||||
- 双栏 Diff + Accept/Reject 按钮
|
|
||||||
- 不弹系统窗口,全部在面板内操作
|
|
||||||
- 时间: 2026-06-17 22:40
|
|
||||||
|
|
||||||
## 第四轮测试
|
|
||||||
|
|
||||||
- 修复命令名: showReviewPanel → showDiffPanel
|
|
||||||
- 修复 Reject: 拒绝时回写原始内容到磁盘
|
|
||||||
- 新增 rejectFileChange 方法
|
|
||||||
- 预期: 点击 Reject 后文件恢复原状
|
|
||||||
|
|
||||||
## 第五轮测试 - 内嵌悬浮窗验证
|
|
||||||
|
|
||||||
这次修改用于验证:
|
|
||||||
|
|
||||||
1. Webview 面板能否正常弹出
|
|
||||||
2. 双栏 Diff 是否正确显示新增/删除行
|
|
||||||
3. Accept 按钮是否生效
|
|
||||||
4. Reject 按钮是否回写原始内容
|
|
||||||
|
|
||||||
测试时间: 2026-06-17 23:00
|
|
||||||
|
|
||||||
## 第六轮测试
|
|
||||||
|
|
||||||
- 修复了所有已知问题
|
|
||||||
- 现在应该能正常工作
|
|
||||||
- 验证: 面板弹出 + 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 按钮正常生效
|
|
||||||
Reference in New Issue
Block a user