Skip to content

Commit 956227e

Browse files
committed
feat: inline edit diff/undo into tool indicators with summary card
Replace separate edit cards at bottom of response with inline Undo/Redo and Show diff controls directly on each Edit/Write tool indicator. - Send per-edit aiToolEdit events from node hooks instead of batched aiEditResult at completion, capturing toolCounter before async to avoid race conditions with the streaming loop - New _onToolEdit handler matches edit events to tool indicators by filename (handles SDK partial re-emissions gracefully) - Undo/Redo button toggles between states with green redo styling - Edit summary card at end of response shows files changed with +/- stats - Fix external change warnings: check file.exists() before disk write in Write handler to avoid triggering file watcher on existing files - Remove _appendEditCard, _onEditResult, and .ai-msg-edit CSS block
1 parent 643ca5a commit 956227e

3 files changed

Lines changed: 304 additions & 201 deletions

File tree

src-node/claude-code-agent.js

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ exports.checkAvailability = async function () {
122122
* Called from browser via execPeer("sendPrompt", {prompt, projectPath, sessionAction, model}).
123123
*
124124
* Returns immediately with a requestId. Results are sent as events:
125-
* aiProgress, aiTextStream, aiEditResult, aiError, aiComplete
125+
* aiProgress, aiTextStream, aiToolEdit, aiError, aiComplete
126126
*/
127127
exports.sendPrompt = async function (params) {
128128
const { prompt, projectPath, sessionAction, model } = params;
@@ -177,7 +177,8 @@ exports.destroySession = async function () {
177177
* Internal: run a Claude SDK query and stream results back to the browser.
178178
*/
179179
async function _runQuery(requestId, prompt, projectPath, model, signal) {
180-
const collectedEdits = [];
180+
let editCount = 0;
181+
let toolCounter = 0;
181182
let queryFn;
182183

183184
try {
@@ -218,17 +219,23 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
218219
hooks: [
219220
async (input) => {
220221
console.log("[Phoenix AI] Intercepted Edit tool");
222+
const myToolId = toolCounter; // capture before any await
221223
const edit = {
222224
file: input.tool_input.file_path,
223225
oldText: input.tool_input.old_string,
224226
newText: input.tool_input.new_string
225227
};
226-
collectedEdits.push(edit);
228+
editCount++;
227229
try {
228230
await nodeConnector.execPeer("applyEditToBuffer", edit);
229231
} catch (err) {
230232
console.warn("[Phoenix AI] Failed to apply edit to buffer:", err.message);
231233
}
234+
nodeConnector.triggerPeer("aiToolEdit", {
235+
requestId: requestId,
236+
toolId: myToolId,
237+
edit: edit
238+
});
232239
return {
233240
hookSpecificOutput: {
234241
hookEventName: "PreToolUse",
@@ -285,17 +292,23 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
285292
hooks: [
286293
async (input) => {
287294
console.log("[Phoenix AI] Intercepted Write tool");
295+
const myToolId = toolCounter; // capture before any await
288296
const edit = {
289297
file: input.tool_input.file_path,
290298
oldText: null,
291299
newText: input.tool_input.content
292300
};
293-
collectedEdits.push(edit);
301+
editCount++;
294302
try {
295303
await nodeConnector.execPeer("applyEditToBuffer", edit);
296304
} catch (err) {
297305
console.warn("[Phoenix AI] Failed to apply write to buffer:", err.message);
298306
}
307+
nodeConnector.triggerPeer("aiToolEdit", {
308+
requestId: requestId,
309+
toolId: myToolId,
310+
edit: edit
311+
});
299312
return {
300313
hookSpecificOutput: {
301314
hookEventName: "PreToolUse",
@@ -342,7 +355,6 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
342355
let activeToolName = null;
343356
let activeToolIndex = null;
344357
let activeToolInputJson = "";
345-
let toolCounter = 0;
346358
let lastToolStreamTime = 0;
347359

348360
// Trace counters (logged at tool/query completion, not per-delta)
@@ -469,15 +481,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
469481
});
470482
}
471483

472-
// Send collected edits if any
473-
if (collectedEdits.length > 0) {
474-
nodeConnector.triggerPeer("aiEditResult", {
475-
requestId: requestId,
476-
edits: collectedEdits
477-
});
478-
}
479-
480-
_log("Complete: tools=" + toolCounter, "edits=" + collectedEdits.length,
484+
_log("Complete: tools=" + toolCounter, "edits=" + editCount,
481485
"textDeltas=" + textDeltaCount, "textSent=" + textStreamSendCount);
482486

483487
// Signal completion
@@ -503,14 +507,6 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
503507

504508
_log("Error:", errMsg.slice(0, 200));
505509

506-
// If we collected edits before error, send them
507-
if (collectedEdits.length > 0) {
508-
nodeConnector.triggerPeer("aiEditResult", {
509-
requestId: requestId,
510-
edits: collectedEdits
511-
});
512-
}
513-
514510
nodeConnector.triggerPeer("aiError", {
515511
requestId: requestId,
516512
error: errMsg

0 commit comments

Comments
 (0)