Skip to content

Commit 643ca5a

Browse files
committed
chore: add AI trace logging, tool preview improvements, and Edit preference
Add compact trace logging for AI events on both node and browser sides for debugging without flooding the console. Fix _finishActiveTools to not override the delayed timeout set by _updateToolIndicator. Add "receiving N bytes..." fallback preview during tool input streaming. Instruct model to prefer Edit over Write for existing files via appendSystemPrompt. Add timestamps to MCP browser console log output.
1 parent 5cbc162 commit 643ca5a

3 files changed

Lines changed: 105 additions & 15 deletions

File tree

phoenix-builder-mcp/mcp-tools.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,16 @@ export function registerTools(server, processManager, wsControlServer, phoenixDe
179179
const totalEntries = result.totalEntries || entries.length;
180180
const matchedEntries = result.matchedEntries != null ? result.matchedEntries : entries.length;
181181
const rangeEnd = result.rangeEnd != null ? result.rangeEnd : matchedEntries;
182-
let lines = entries.map(e => `[${e.level}] ${e.message}`);
182+
let lines = entries.map(e => {
183+
let ts = "";
184+
if (e.timestamp) {
185+
// Show HH:MM:SS.mmm for compact display
186+
const d = new Date(e.timestamp);
187+
ts = d.toTimeString().slice(0, 8) + "." +
188+
String(d.getMilliseconds()).padStart(3, "0") + " ";
189+
}
190+
return `[${ts}${e.level}] ${e.message}`;
191+
});
183192
let trimmed = 0;
184193
if (maxChars > 0) {
185194
const trimResult = _trimToCharBudget(lines, maxChars);

src-node/claude-code-agent.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,13 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
202202
maxTurns: 10,
203203
allowedTools: ["Read", "Edit", "Write", "Glob", "Grep"],
204204
permissionMode: "acceptEdits",
205+
appendSystemPrompt:
206+
"When modifying an existing file, always prefer the Edit tool " +
207+
"(find-and-replace) instead of the Write tool. The Write tool should ONLY be used " +
208+
"to create brand new files that do not exist yet. For existing files, always use " +
209+
"multiple Edit calls to make targeted changes rather than rewriting the entire " +
210+
"file with Write. This is critical because Write replaces the entire file content " +
211+
"which is slow and loses undo history.",
205212
includePartialMessages: true,
206213
abortController: currentAbortController,
207214
hooks: {
@@ -318,7 +325,11 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
318325
queryOptions.resume = currentSessionId;
319326
}
320327

328+
const _log = (...args) => console.log("[AI]", ...args);
329+
321330
try {
331+
_log("Query start:", JSON.stringify(prompt).slice(0, 80), "cwd=" + (projectPath || "?"));
332+
322333
const result = queryFn({
323334
prompt: prompt,
324335
options: queryOptions
@@ -334,15 +345,23 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
334345
let toolCounter = 0;
335346
let lastToolStreamTime = 0;
336347

348+
// Trace counters (logged at tool/query completion, not per-delta)
349+
let toolDeltaCount = 0;
350+
let toolStreamSendCount = 0;
351+
let textDeltaCount = 0;
352+
let textStreamSendCount = 0;
353+
337354
for await (const message of result) {
338355
// Check abort
339356
if (signal.aborted) {
357+
_log("Aborted");
340358
break;
341359
}
342360

343361
// Capture session_id from first message
344362
if (message.session_id && !currentSessionId) {
345363
currentSessionId = message.session_id;
364+
_log("Session:", currentSessionId);
346365
}
347366

348367
// Handle streaming events
@@ -356,6 +375,10 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
356375
activeToolIndex = event.index;
357376
activeToolInputJson = "";
358377
toolCounter++;
378+
toolDeltaCount = 0;
379+
toolStreamSendCount = 0;
380+
lastToolStreamTime = 0;
381+
_log("Tool start:", activeToolName, "#" + toolCounter);
359382
nodeConnector.triggerPeer("aiProgress", {
360383
requestId: requestId,
361384
toolName: activeToolName,
@@ -369,9 +392,12 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
369392
event.delta?.type === "input_json_delta" &&
370393
event.index === activeToolIndex) {
371394
activeToolInputJson += event.delta.partial_json;
395+
toolDeltaCount++;
372396
const now = Date.now();
373-
if (now - lastToolStreamTime >= TEXT_STREAM_THROTTLE_MS) {
397+
if (activeToolInputJson &&
398+
now - lastToolStreamTime >= TEXT_STREAM_THROTTLE_MS) {
374399
lastToolStreamTime = now;
400+
toolStreamSendCount++;
375401
nodeConnector.triggerPeer("aiToolStream", {
376402
requestId: requestId,
377403
toolId: toolCounter,
@@ -387,6 +413,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
387413
activeToolName) {
388414
// Final flush of tool stream (bypasses throttle)
389415
if (activeToolInputJson) {
416+
toolStreamSendCount++;
390417
nodeConnector.triggerPeer("aiToolStream", {
391418
requestId: requestId,
392419
toolId: toolCounter,
@@ -400,6 +427,9 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
400427
} catch (e) {
401428
// ignore parse errors
402429
}
430+
_log("Tool done:", activeToolName, "#" + toolCounter,
431+
"deltas=" + toolDeltaCount, "sent=" + toolStreamSendCount,
432+
"json=" + activeToolInputJson.length + "ch");
403433
nodeConnector.triggerPeer("aiToolInfo", {
404434
requestId: requestId,
405435
toolName: activeToolName,
@@ -415,9 +445,11 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
415445
if (event.type === "content_block_delta" &&
416446
event.delta?.type === "text_delta") {
417447
accumulatedText += event.delta.text;
448+
textDeltaCount++;
418449
const now = Date.now();
419450
if (now - lastStreamTime >= TEXT_STREAM_THROTTLE_MS) {
420451
lastStreamTime = now;
452+
textStreamSendCount++;
421453
nodeConnector.triggerPeer("aiTextStream", {
422454
requestId: requestId,
423455
text: accumulatedText
@@ -430,6 +462,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
430462

431463
// Flush any remaining accumulated text
432464
if (accumulatedText) {
465+
textStreamSendCount++;
433466
nodeConnector.triggerPeer("aiTextStream", {
434467
requestId: requestId,
435468
text: accumulatedText
@@ -444,6 +477,9 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
444477
});
445478
}
446479

480+
_log("Complete: tools=" + toolCounter, "edits=" + collectedEdits.length,
481+
"textDeltas=" + textDeltaCount, "textSent=" + textStreamSendCount);
482+
447483
// Signal completion
448484
nodeConnector.triggerPeer("aiComplete", {
449485
requestId: requestId,
@@ -455,6 +491,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
455491
const isAbort = signal.aborted || /abort/i.test(errMsg);
456492

457493
if (isAbort) {
494+
_log("Cancelled");
458495
// Query was cancelled — clear session so next query starts fresh
459496
currentSessionId = null;
460497
nodeConnector.triggerPeer("aiComplete", {
@@ -464,6 +501,8 @@ async function _runQuery(requestId, prompt, projectPath, model, signal) {
464501
return;
465502
}
466503

504+
_log("Error:", errMsg.slice(0, 200));
505+
467506
// If we collected edits before error, send them
468507
if (collectedEdits.length > 0) {
469508
nodeConnector.triggerPeer("aiEditResult", {

src/core-ai/AIChatPanel.js

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ define(function (require, exports, module) {
4040
let _hasReceivedContent = false; // tracks if we've received any text/tool in current response
4141
const _previousContentMap = {}; // filePath → previous content before edit, for undo support
4242

43+
// --- AI event trace logging (compact, non-flooding) ---
44+
let _traceTextChunks = 0;
45+
let _traceToolStreamCounts = {}; // toolId → count
46+
4347
// DOM references
4448
let $panel, $messages, $status, $statusText, $textarea, $sendBtn, $stopBtn;
4549

@@ -230,12 +234,17 @@ define(function (require, exports, module) {
230234
// Get project path
231235
const projectPath = _getProjectRealPath();
232236

237+
_traceTextChunks = 0;
238+
_traceToolStreamCounts = {};
239+
console.log("[AI UI] Sending prompt:", text.slice(0, 60));
240+
233241
_nodeConnector.execPeer("sendPrompt", {
234242
prompt: text,
235243
projectPath: projectPath,
236244
sessionAction: "continue"
237245
}).then(function (result) {
238246
_currentRequestId = result.requestId;
247+
console.log("[AI UI] RequestId:", result.requestId);
239248
}).catch(function (err) {
240249
_setStreaming(false);
241250
_appendErrorMessage("Failed to send message: " + (err.message || String(err)));
@@ -288,6 +297,11 @@ define(function (require, exports, module) {
288297
// --- Event handlers for node-side events ---
289298

290299
function _onTextStream(_event, data) {
300+
_traceTextChunks++;
301+
if (_traceTextChunks === 1) {
302+
console.log("[AI UI]", "First text chunk");
303+
}
304+
291305
// Remove thinking indicator on first content
292306
if (!_hasReceivedContent) {
293307
_hasReceivedContent = true;
@@ -315,6 +329,7 @@ define(function (require, exports, module) {
315329
};
316330

317331
function _onProgress(_event, data) {
332+
console.log("[AI UI]", "Progress:", data.phase, data.toolName ? data.toolName + " #" + data.toolId : "");
318333
if ($statusText) {
319334
const toolName = data.toolName || "";
320335
const config = TOOL_CONFIG[toolName];
@@ -326,11 +341,17 @@ define(function (require, exports, module) {
326341
}
327342

328343
function _onToolInfo(_event, data) {
344+
const uid = (_currentRequestId || "") + "-" + data.toolId;
345+
const streamCount = _traceToolStreamCounts[uid] || 0;
346+
console.log("[AI UI]", "ToolInfo:", data.toolName, "#" + data.toolId,
347+
"file=" + (data.toolInput && data.toolInput.file_path || "?").split("/").pop(),
348+
"streamEvents=" + streamCount);
329349
_updateToolIndicator(data.toolId, data.toolName, data.toolInput);
330350
}
331351

332352
function _onToolStream(_event, data) {
333353
const uniqueToolId = (_currentRequestId || "") + "-" + data.toolId;
354+
_traceToolStreamCounts[uniqueToolId] = (_traceToolStreamCounts[uniqueToolId] || 0) + 1;
334355
const $tool = $messages.find('.ai-msg-tool[data-tool-id="' + uniqueToolId + '"]');
335356
if (!$tool.length) {
336357
return;
@@ -348,6 +369,10 @@ define(function (require, exports, module) {
348369
}
349370

350371
const preview = _extractToolPreview(data.toolName, data.partialJson);
372+
if (_traceToolStreamCounts[uniqueToolId] === 1) {
373+
console.log("[AI UI]", "ToolStream first:", data.toolName, "#" + data.toolId,
374+
"json=" + (data.partialJson || "").length + "ch");
375+
}
351376
if (preview) {
352377
$tool.find(".ai-tool-preview").text(preview);
353378
_scrollToBottom();
@@ -389,7 +414,8 @@ define(function (require, exports, module) {
389414
if (!partialJson) {
390415
return "";
391416
}
392-
// Map tool names to the key whose value we want to preview
417+
// Map tool names to the key whose value we want to preview.
418+
// Tools not listed here get no streaming preview.
393419
const interestingKey = {
394420
Write: "content",
395421
Edit: "new_string",
@@ -398,19 +424,21 @@ define(function (require, exports, module) {
398424
Glob: "pattern"
399425
}[toolName];
400426

427+
if (!interestingKey) {
428+
return "";
429+
}
430+
401431
let raw = "";
402-
if (interestingKey) {
403-
// Find the interesting key and grab everything after it
404-
const keyPattern = '"' + interestingKey + '":';
405-
const idx = partialJson.indexOf(keyPattern);
406-
if (idx !== -1) {
407-
raw = partialJson.slice(idx + keyPattern.length).slice(-120);
408-
}
409-
// If the interesting key hasn't appeared yet, show nothing
410-
// rather than raw JSON noise like {"file_path":...
411-
} else {
412-
// No interesting key defined for this tool — use the tail
413-
raw = partialJson.slice(-120);
432+
// Find the interesting key and grab everything after it
433+
const keyPattern = '"' + interestingKey + '":';
434+
const idx = partialJson.indexOf(keyPattern);
435+
if (idx !== -1) {
436+
raw = partialJson.slice(idx + keyPattern.length).slice(-120);
437+
}
438+
// If the interesting key hasn't appeared yet, show a byte counter
439+
// so the user sees streaming activity during the file_path phase
440+
if (!raw && partialJson.length > 3) {
441+
return "receiving " + partialJson.length + " bytes...";
414442
}
415443
if (!raw) {
416444
return "";
@@ -430,6 +458,7 @@ define(function (require, exports, module) {
430458
}
431459

432460
function _onEditResult(_event, data) {
461+
console.log("[AI UI]", "EditResult:", (data.edits || []).length, "edits");
433462
if (data.edits && data.edits.length > 0) {
434463
data.edits.forEach(function (edit) {
435464
_appendEditCard(edit);
@@ -438,11 +467,17 @@ define(function (require, exports, module) {
438467
}
439468

440469
function _onError(_event, data) {
470+
console.log("[AI UI]", "Error:", (data.error || "").slice(0, 200));
441471
_appendErrorMessage(data.error);
442472
// Don't stop streaming — the node side may continue (partial results)
443473
}
444474

445475
function _onComplete(_event, data) {
476+
console.log("[AI UI]", "Complete. textChunks=" + _traceTextChunks,
477+
"toolStreams=" + JSON.stringify(_traceToolStreamCounts));
478+
// Reset trace counters for next query
479+
_traceTextChunks = 0;
480+
_traceToolStreamCounts = {};
446481
_setStreaming(false);
447482
}
448483

@@ -654,10 +689,17 @@ define(function (require, exports, module) {
654689

655690
/**
656691
* Mark all active (non-done) tool indicators as finished.
692+
* Tools that already received _updateToolIndicator (spinner replaced with
693+
* .ai-tool-icon) are skipped — their delayed timeout will add .ai-tool-done.
694+
* This only force-finishes tools that never got a toolInfo (e.g. interrupted).
657695
*/
658696
function _finishActiveTools() {
659697
$messages.find(".ai-msg-tool:not(.ai-tool-done)").each(function () {
660698
const $prev = $(this);
699+
// _updateToolIndicator already ran — let the delayed timeout handle it
700+
if ($prev.find(".ai-tool-icon").length) {
701+
return;
702+
}
661703
$prev.addClass("ai-tool-done");
662704
const iconClass = $prev.attr("data-tool-icon") || "fa-solid fa-check";
663705
const color = $prev.css("--tool-color") || "#adb9bd";

0 commit comments

Comments
 (0)