mirror of
https://github.com/Gmaker689/ai-diff-preview.git
synced 2026-09-27 03:21:52 +08:00
refactor: 用 StopFailure 事件替代定时轮询,移除空闲检测定时器
- 移除 heartbeat 空闲检测机制(PreToolUse 空 matcher) - 新增 StopFailure hooks 处理 API 错误中断场景 - PostToolUse 仅保留 Edit(Write 通过 PreToolUse 快照实现) - 移除 extension.ts 中 idleTimer/idleCooldown/heartbeatWatcher - 更新 README hooks 配置文档 - 添加 MIT LICENSE
This commit is contained in:
+26
-3
@@ -1,12 +1,24 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"matcher": "Write",
|
||||
"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||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
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"matcher": "Write",
|
||||
"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||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
|
||||
}
|
||||
]
|
||||
@@ -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: 下一个变更` | `Alt+↓` | 跳转下一个 pending 文件 |
|
||||
| `AI Diff: 上一个变更` | `Alt+↑` | 跳转上一个 pending 文件 |
|
||||
| `AI Diff: 接受所有变更` | `Ctrl+Shift+A` | Accept All |
|
||||
| `AI Diff: 拒绝所有变更` | `Ctrl+Shift+R` | Reject All |
|
||||
| `AI Diff: 下一个变更` | `Alt+↓` | 跳转下一个 pending 文件 |
|
||||
| `AI Diff: 上一个变更` | `Alt+↑` | 跳转上一个 pending 文件 |
|
||||
| `AI Diff: 接受所有变更` | `Ctrl+Shift+A` | Accept All |
|
||||
| `AI Diff: 拒绝所有变更` | `Ctrl+Shift+R` | Reject All |
|
||||
|
||||
Diff 编辑器打开后,状态栏右侧自动出现 **Accept / Reject / 接受全部** 按钮。
|
||||
|
||||
|
||||
+44
-10
@@ -30,6 +30,10 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
hookHandler = new HookHandler(changeSetManager, diffViewer);
|
||||
statusBar = new StatusBarManager(changeSetManager, diffViewer);
|
||||
|
||||
// ★ 设置 hook 目录路径,供 snapshotManager 读取 PreToolUse 快照
|
||||
const hookDir = getHookDir();
|
||||
if (hookDir) changeSetManager.setHookDir(hookDir);
|
||||
|
||||
// DiffViewer 操作后刷新状态栏
|
||||
diffViewer.onDiffAction = () => refreshUI();
|
||||
|
||||
@@ -95,6 +99,24 @@ function registerCommands(context: vscode.ExtensionContext) {
|
||||
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 界面状态栏按钮
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('aiDiffPreview.acceptCurrentDiff', async () => {
|
||||
@@ -150,6 +172,14 @@ function refreshUI(): void {
|
||||
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 监听 ----
|
||||
|
||||
function registerHookFileWatcher(context: vscode.ExtensionContext) {
|
||||
@@ -160,11 +190,13 @@ function registerHookFileWatcher(context: vscode.ExtensionContext) {
|
||||
if (!fs.existsSync(hookDir)) fs.mkdirSync(hookDir, { recursive: true });
|
||||
|
||||
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')
|
||||
);
|
||||
|
||||
const handle = () => {
|
||||
const handlePending = () => {
|
||||
try {
|
||||
if (!fs.existsSync(pendingFile)) return;
|
||||
const content = fs.readFileSync(pendingFile, 'utf-8');
|
||||
@@ -172,18 +204,20 @@ function registerHookFileWatcher(context: vscode.ExtensionContext) {
|
||||
const event: ClaudeHookEvent = JSON.parse(content);
|
||||
if (Date.now() - event.timestamp > 10000) return;
|
||||
|
||||
if (event.toolName === 'Stop') {
|
||||
if (event.toolName === 'Stop' || event.toolName === 'StopFailure') {
|
||||
// ★ 对话结束(正常完成或 API 失败)→ 触发 diff 总览
|
||||
hookHandler.handleStop();
|
||||
} else if (event.toolName === 'Edit' || event.toolName === 'Write') {
|
||||
} else if (event.toolName === 'Edit') {
|
||||
hookHandler.handleEdit(event);
|
||||
}
|
||||
refreshUI();
|
||||
} catch (e) { console.error('[AI Diff] 处理触发文件失败:', e); }
|
||||
} catch (e) { console.error('[AI Diff] 处理 pending 文件失败:', e); }
|
||||
};
|
||||
|
||||
watcher.onDidChange(handle);
|
||||
watcher.onDidCreate(handle);
|
||||
context.subscriptions.push(watcher);
|
||||
pendingWatcher.onDidChange(handlePending);
|
||||
pendingWatcher.onDidCreate(handlePending);
|
||||
context.subscriptions.push(pendingWatcher);
|
||||
|
||||
console.log(`[AI Diff] Hook 监听已启动: ${hookDir}`);
|
||||
}
|
||||
|
||||
@@ -199,8 +233,8 @@ function checkPendingHookFile() {
|
||||
fs.unlinkSync(pendingFile);
|
||||
const event: ClaudeHookEvent = JSON.parse(content);
|
||||
if (Date.now() - event.timestamp < 15000) {
|
||||
if (event.toolName === 'Stop') hookHandler.handleStop();
|
||||
else if (event.toolName === 'Edit' || event.toolName === 'Write') hookHandler.handleEdit(event);
|
||||
if (event.toolName === 'Stop' || event.toolName === 'StopFailure') hookHandler.handleStop();
|
||||
else if (event.toolName === 'Edit') hookHandler.handleEdit(event);
|
||||
refreshUI();
|
||||
}
|
||||
} catch (e) { console.error('[AI Diff] 处理 pending hook 失败:', e); }
|
||||
|
||||
Reference in New Issue
Block a user