feat(acp): forward meta/locations fields and use parentToolUseId for precise Agent child grouping

Forward the previously-dropped `locations` and `meta` fields from ACP
SDK ToolCall/ToolCallUpdate events through to the frontend. The meta
field carries `claudeCode.parentToolUseId` which enables precise
parent-child matching for concurrent Agent tool calls during streaming.

- Forward locations/meta in Rust AcpEvent types and connection handlers
- Use parentToolUseId for exact agent→child mapping, with position-based
  fallback for agents that don't provide it (Codex, OpenCode)
- Replace `any` types with proper ToolCallMeta / unknown types
- Add runtime guards for meta field parsing (defensive against
  unexpected shapes from different agents)
- Cache inferLiveToolName results per tool_call_id to avoid redundant
  computation across Phase 1 and Phase 2
- Lazy-construct agentStats only when children exist
This commit is contained in:
xintaofei
2026-04-17 08:24:12 +08:00
parent 6763814a92
commit 834340e536
5 changed files with 141 additions and 58 deletions

View File

@@ -1554,6 +1554,8 @@ fn emit_terminal_output_update(
raw_input: None,
raw_output: Some(output),
raw_output_append: Some(append),
locations: None,
meta: None,
},
);
}
@@ -2501,6 +2503,12 @@ fn emit_conversation_update(
.map(|text| resolve_live_tool_input(&text, cwd));
let raw_output = json_value_to_text(&tc.raw_output)
.map(|text| structurize_live_output(&text));
let locations = if tc.locations.is_empty() {
None
} else {
serde_json::to_value(&tc.locations).ok()
};
let meta = tc.meta.map(serde_json::Value::Object);
crate::web::event_bridge::emit_event(
emitter,
"acp://event",
@@ -2513,6 +2521,8 @@ fn emit_conversation_update(
content,
raw_input,
raw_output,
locations,
meta,
},
);
}
@@ -2526,6 +2536,13 @@ fn emit_conversation_update(
.map(|text| resolve_live_tool_input(&text, cwd));
let raw_output = json_value_to_text(&tcu.fields.raw_output)
.map(|text| structurize_live_output(&text));
let locations = tcu
.fields
.locations
.as_ref()
.filter(|l| !l.is_empty())
.and_then(|l| serde_json::to_value(l).ok());
let meta = tcu.meta.clone().map(serde_json::Value::Object);
crate::web::event_bridge::emit_event(
emitter,
"acp://event",
@@ -2538,6 +2555,8 @@ fn emit_conversation_update(
raw_input,
raw_output,
raw_output_append: None,
locations,
meta,
},
);
}

View File

@@ -63,6 +63,10 @@ pub enum AcpEvent {
content: Option<String>,
raw_input: Option<String>,
raw_output: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
locations: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
meta: Option<serde_json::Value>,
},
/// Tool call status/content updated
ToolCallUpdate {
@@ -75,6 +79,10 @@ pub enum AcpEvent {
raw_output: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
raw_output_append: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
locations: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
meta: Option<serde_json::Value>,
},
/// Agent requests permission
PermissionRequest {