Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 170 additions & 74 deletions src-node/claude-code-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,12 +378,18 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale)
let accumulatedText = "";
let lastStreamTime = 0;

// Tool input tracking
// Tool input tracking (parent-level)
let activeToolName = null;
let activeToolIndex = null;
let activeToolInputJson = "";
let lastToolStreamTime = 0;

// Sub-agent tool tracking
let subagentToolName = null;
let subagentToolIndex = null;
let subagentToolInputJson = "";
let lastSubagentToolStreamTime = 0;

// Trace counters (logged at tool/query completion, not per-delta)
let toolDeltaCount = 0;
let toolStreamSendCount = 0;
Expand All @@ -406,94 +412,184 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale)
// Handle streaming events
if (message.type === "stream_event") {
const event = message.event;
const isSubagent = !!message.parent_tool_use_id;

if (isSubagent) {
// --- Sub-agent events ---

// Sub-agent tool use start
if (event.type === "content_block_start" &&
event.content_block?.type === "tool_use") {
subagentToolName = event.content_block.name;
subagentToolIndex = event.index;
subagentToolInputJson = "";
toolCounter++;
lastSubagentToolStreamTime = 0;
_log("Subagent tool start:", subagentToolName, "#" + toolCounter);
nodeConnector.triggerPeer("aiProgress", {
requestId: requestId,
toolName: subagentToolName,
toolId: toolCounter,
phase: "tool_use"
});
}

// Tool use start — send initial indicator
if (event.type === "content_block_start" &&
event.content_block?.type === "tool_use") {
activeToolName = event.content_block.name;
activeToolIndex = event.index;
activeToolInputJson = "";
toolCounter++;
toolDeltaCount = 0;
toolStreamSendCount = 0;
lastToolStreamTime = 0;
_log("Tool start:", activeToolName, "#" + toolCounter);
nodeConnector.triggerPeer("aiProgress", {
requestId: requestId,
toolName: activeToolName,
toolId: toolCounter,
phase: "tool_use"
});
}
// Sub-agent tool input streaming
if (event.type === "content_block_delta" &&
event.delta?.type === "input_json_delta" &&
event.index === subagentToolIndex) {
subagentToolInputJson += event.delta.partial_json;
const now = Date.now();
if (subagentToolInputJson &&
now - lastSubagentToolStreamTime >= TEXT_STREAM_THROTTLE_MS) {
lastSubagentToolStreamTime = now;
nodeConnector.triggerPeer("aiToolStream", {
requestId: requestId,
toolId: toolCounter,
toolName: subagentToolName,
partialJson: subagentToolInputJson
});
}
}

// Accumulate tool input JSON and stream preview
if (event.type === "content_block_delta" &&
event.delta?.type === "input_json_delta" &&
event.index === activeToolIndex) {
activeToolInputJson += event.delta.partial_json;
toolDeltaCount++;
const now = Date.now();
if (activeToolInputJson &&
now - lastToolStreamTime >= TEXT_STREAM_THROTTLE_MS) {
lastToolStreamTime = now;
toolStreamSendCount++;
nodeConnector.triggerPeer("aiToolStream", {
// Sub-agent tool block complete
if (event.type === "content_block_stop" &&
event.index === subagentToolIndex &&
subagentToolName) {
if (subagentToolInputJson) {
nodeConnector.triggerPeer("aiToolStream", {
requestId: requestId,
toolId: toolCounter,
toolName: subagentToolName,
partialJson: subagentToolInputJson
});
}
let toolInput = {};
try {
toolInput = JSON.parse(subagentToolInputJson);
} catch (e) {
// ignore parse errors
}
_log("Subagent tool done:", subagentToolName, "#" + toolCounter,
"json=" + subagentToolInputJson.length + "ch");
nodeConnector.triggerPeer("aiToolInfo", {
requestId: requestId,
toolName: subagentToolName,
toolId: toolCounter,
toolName: activeToolName,
partialJson: activeToolInputJson
toolInput: toolInput
});
subagentToolName = null;
subagentToolIndex = null;
subagentToolInputJson = "";
}
}

// Tool block complete — flush final stream preview and send details
if (event.type === "content_block_stop" &&
event.index === activeToolIndex &&
activeToolName) {
// Final flush of tool stream (bypasses throttle)
if (activeToolInputJson) {
toolStreamSendCount++;
nodeConnector.triggerPeer("aiToolStream", {
// Sub-agent text deltas — stream as regular text
if (event.type === "content_block_delta" &&
event.delta?.type === "text_delta") {
accumulatedText += event.delta.text;
textDeltaCount++;
const now = Date.now();
if (now - lastStreamTime >= TEXT_STREAM_THROTTLE_MS) {
lastStreamTime = now;
textStreamSendCount++;
nodeConnector.triggerPeer("aiTextStream", {
requestId: requestId,
text: accumulatedText
});
accumulatedText = "";
}
}
} else {
// --- Parent-level events (unchanged) ---

// Tool use start — send initial indicator
if (event.type === "content_block_start" &&
event.content_block?.type === "tool_use") {
activeToolName = event.content_block.name;
activeToolIndex = event.index;
activeToolInputJson = "";
toolCounter++;
toolDeltaCount = 0;
toolStreamSendCount = 0;
lastToolStreamTime = 0;
_log("Tool start:", activeToolName, "#" + toolCounter);
nodeConnector.triggerPeer("aiProgress", {
requestId: requestId,
toolId: toolCounter,
toolName: activeToolName,
partialJson: activeToolInputJson
toolId: toolCounter,
phase: "tool_use"
});
}
let toolInput = {};
try {
toolInput = JSON.parse(activeToolInputJson);
} catch (e) {
// ignore parse errors

// Accumulate tool input JSON and stream preview
if (event.type === "content_block_delta" &&
event.delta?.type === "input_json_delta" &&
event.index === activeToolIndex) {
activeToolInputJson += event.delta.partial_json;
toolDeltaCount++;
const now = Date.now();
if (activeToolInputJson &&
now - lastToolStreamTime >= TEXT_STREAM_THROTTLE_MS) {
lastToolStreamTime = now;
toolStreamSendCount++;
nodeConnector.triggerPeer("aiToolStream", {
requestId: requestId,
toolId: toolCounter,
toolName: activeToolName,
partialJson: activeToolInputJson
});
}
}
_log("Tool done:", activeToolName, "#" + toolCounter,
"deltas=" + toolDeltaCount, "sent=" + toolStreamSendCount,
"json=" + activeToolInputJson.length + "ch");
nodeConnector.triggerPeer("aiToolInfo", {
requestId: requestId,
toolName: activeToolName,
toolId: toolCounter,
toolInput: toolInput
});
activeToolName = null;
activeToolIndex = null;
activeToolInputJson = "";
}

// Stream text deltas (throttled)
if (event.type === "content_block_delta" &&
event.delta?.type === "text_delta") {
accumulatedText += event.delta.text;
textDeltaCount++;
const now = Date.now();
if (now - lastStreamTime >= TEXT_STREAM_THROTTLE_MS) {
lastStreamTime = now;
textStreamSendCount++;
nodeConnector.triggerPeer("aiTextStream", {
// Tool block complete — flush final stream preview and send details
if (event.type === "content_block_stop" &&
event.index === activeToolIndex &&
activeToolName) {
// Final flush of tool stream (bypasses throttle)
if (activeToolInputJson) {
toolStreamSendCount++;
nodeConnector.triggerPeer("aiToolStream", {
requestId: requestId,
toolId: toolCounter,
toolName: activeToolName,
partialJson: activeToolInputJson
});
}
let toolInput = {};
try {
toolInput = JSON.parse(activeToolInputJson);
} catch (e) {
// ignore parse errors
}
_log("Tool done:", activeToolName, "#" + toolCounter,
"deltas=" + toolDeltaCount, "sent=" + toolStreamSendCount,
"json=" + activeToolInputJson.length + "ch");
nodeConnector.triggerPeer("aiToolInfo", {
requestId: requestId,
text: accumulatedText
toolName: activeToolName,
toolId: toolCounter,
toolInput: toolInput
});
accumulatedText = "";
activeToolName = null;
activeToolIndex = null;
activeToolInputJson = "";
}

// Stream text deltas (throttled)
if (event.type === "content_block_delta" &&
event.delta?.type === "text_delta") {
accumulatedText += event.delta.text;
textDeltaCount++;
const now = Date.now();
if (now - lastStreamTime >= TEXT_STREAM_THROTTLE_MS) {
lastStreamTime = now;
textStreamSendCount++;
nodeConnector.triggerPeer("aiTextStream", {
requestId: requestId,
text: accumulatedText
});
accumulatedText = "";
}
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion src-node/mcp-editor-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ function createEditorMcpServer(sdkModule, nodeConnector) {
const getEditorStateTool = sdkModule.tool(
"getEditorState",
"Get the current Phoenix editor state: active file, working set (open files), live preview file, " +
"and cursor/selection info (current line text with surrounding context, or selected text). " +
"cursor/selection info (current line text with surrounding context, or selected text), " +
"and the currently selected element in the live preview (tag, selector, text preview) if any. " +
"The live preview selected element may differ from the editor cursor — use execJsInLivePreview to inspect it further. " +
"Long lines are trimmed to 200 chars and selections to 10K chars — use the Read tool for full content.",
{},
async function () {
Expand Down
33 changes: 32 additions & 1 deletion src/core-ai/aiPhoenixConnectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ define(function (require, exports, module) {
* Called from the node-side MCP server via execPeer.
*/
function getEditorState() {
const deferred = new $.Deferred();
const activeFile = MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE);
const workingSet = MainViewManager.getWorkingSet(MainViewManager.ALL_PANES);

Expand Down Expand Up @@ -138,7 +139,37 @@ define(function (require, exports, module) {
}
}

return result;
// If live preview is connected, query the selected element (best-effort)
if (LiveDevProtocol.getConnectionIds().length > 0) {
const LP_SELECTED_EL_JS =
"(function(){" +
"var el=window.__current_ph_lp_selected;" +
"if(!el||!el.isConnected)return null;" +
"var tag=el.tagName.toLowerCase();" +
"var id=el.id?'#'+el.id:'';" +
"var cls=el.className&&typeof el.className==='string'?" +
"'.'+el.className.trim().split(/\\s+/).join('.'):" +
"'';" +
"var text=el.textContent||'';" +
"if(text.length>80)text=text.slice(0,80)+'...';" +
"text=text.replace(/\\n/g,' ').trim();" +
"return{tag:tag,selector:tag+id+cls,textPreview:text};" +
"})()";
LiveDevProtocol.evaluate(LP_SELECTED_EL_JS)
.done(function (evalResult) {
if (evalResult && evalResult.tag) {
result.livePreviewSelectedElement = evalResult;
}
deferred.resolve(result);
})
.fail(function () {
deferred.resolve(result);
});
} else {
deferred.resolve(result);
}

return deferred.promise();
}

// --- Screenshot ---
Expand Down
2 changes: 1 addition & 1 deletion src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -1825,7 +1825,7 @@ define({
"AI_CHAT_TOOL_SCREENSHOT_OF": "Screenshot of {0}",
"AI_CHAT_TOOL_SCREENSHOT_LIVE_PREVIEW": "live preview",
"AI_CHAT_TOOL_SCREENSHOT_FULL_PAGE": "full page",
"AI_CHAT_TOOL_LIVE_PREVIEW_JS": "Live Preview JS",
"AI_CHAT_TOOL_LIVE_PREVIEW_JS": "Inspecting preview",
"AI_CHAT_TOOL_CONTROL_EDITOR": "Editor",
"AI_CHAT_TOOL_TASKS": "Tasks",
"AI_CHAT_TOOL_TASKS_SUMMARY": "{0} of {1} tasks done",
Expand Down
5 changes: 4 additions & 1 deletion src/styles/Extn-AIChatPanel.less
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@

/* ── TodoWrite task list widget ────────────────────────────────────── */
.ai-todo-list {
padding: 2px 8px 4px 28px;
padding: 2px 8px 4px 0;
}

.ai-todo-item {
Expand All @@ -434,6 +434,9 @@
.ai-todo-content {
color: @project-panel-text-2;
line-height: 1.4;
white-space: normal;
overflow-wrap: break-word;
min-width: 0;

&.completed {
text-decoration: line-through;
Expand Down
Loading