mirror of
https://github.com/Gmaker689/ai-diff-preview.git
synced 2026-09-26 23:11:51 +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": {
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,15 +56,35 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"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 文件 |
|
||||||
|
|||||||
+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); }
|
||||||
|
|||||||
Reference in New Issue
Block a user