feat: add agent_type field and support Agent tool rename from CLI v2.1.69

- Parse agent_type from SubagentStart hook events and forward via claude:agent-update
- Support renamed Agent/Agent(type) tool alongside existing Task/Task(type) syntax
- Fall back to prompt field when description is absent in Agent tool input
- Display agentType (with subagentType fallback) in agent monitor badge
- Show agentId as tooltip on the type badge when available
This commit is contained in:
2026-03-11 11:21:43 -07:00
committed by Naomi Carrigan
parent 1f8825b0cb
commit 021269983d
6 changed files with 147 additions and 10 deletions
+117 -6
View File
@@ -913,13 +913,14 @@ fn handle_stderr(
tracing::debug!("Parsed SubagentStart hook: agent_id={}, parent={:?}",
agent_data.agent_id, agent_data.parent_tool_use_id);
// Emit an agent-update event with the agent_id
// Emit an agent-update event with the agent_id and agent_type
let _ = app.emit(
"claude:agent-update",
serde_json::json!({
"conversationId": conversation_id.clone(),
"toolUseId": agent_data.parent_tool_use_id,
"agentId": agent_data.agent_id,
"agentType": agent_data.agent_type,
}),
);
}
@@ -986,11 +987,12 @@ fn handle_stderr(
#[derive(Debug)]
struct SubagentStartData {
agent_id: String,
agent_type: Option<String>,
parent_tool_use_id: Option<String>,
}
fn parse_subagent_start_hook(line: &str) -> Option<SubagentStartData> {
// Parse: [SubagentStart Hook] agent_id=agent-xxx, parent_tool_use_id=Some("toolu_xxx"), ...
// Parse: [SubagentStart Hook] agent_id=agent-xxx, agent_type=general-purpose, parent_tool_use_id=Some("toolu_xxx"), ...
// Extract agent_id
let agent_id = line
@@ -1001,6 +1003,15 @@ fn parse_subagent_start_hook(line: &str) -> Option<SubagentStartData> {
.trim()
.to_string();
// Extract agent_type if present (added in CLI v2.1.69)
let agent_type = line
.split("agent_type=")
.nth(1)
.and_then(|s| {
let value = s.split(',').next()?.trim();
if value.is_empty() { None } else { Some(value.to_string()) }
});
// Extract parent_tool_use_id if present
let parent_tool_use_id = if line.contains("parent_tool_use_id=Some") {
line.split("parent_tool_use_id=Some(\"")
@@ -1014,6 +1025,7 @@ fn parse_subagent_start_hook(line: &str) -> Option<SubagentStartData> {
Some(SubagentStartData {
agent_id,
agent_type,
parent_tool_use_id,
})
}
@@ -1272,11 +1284,15 @@ fn process_json_line(
}
}
// Emit agent-start event for Task tool invocations
// Support both "Task" and "Task(agent_type)" syntax (CLI v2.1.33+)
if name == "Task" || name.starts_with("Task(") {
// Emit agent-start event for Task/Agent tool invocations
// Support "Task"/"Task(agent_type)" (CLI v2.1.33+) and
// "Agent"/"Agent(agent_type)" (CLI v2.1.69+ rename)
if name == "Task" || name.starts_with("Task(")
|| name == "Agent" || name.starts_with("Agent(")
{
let description = input
.get("description")
.or_else(|| input.get("prompt"))
.and_then(|v| v.as_str())
.unwrap_or("Subagent")
.to_string();
@@ -1943,7 +1959,9 @@ fn get_tool_state(tool_name: &str) -> CharacterState {
CharacterState::Coding
} else if tool_name.starts_with("mcp__") {
CharacterState::Mcp
} else if tool_name == "Task" || tool_name.starts_with("Task(") {
} else if tool_name == "Task" || tool_name.starts_with("Task(")
|| tool_name == "Agent" || tool_name.starts_with("Agent(")
{
CharacterState::Thinking
} else {
CharacterState::Typing
@@ -2067,6 +2085,19 @@ fn format_tool_description(name: &str, input: &serde_json::Value) -> String {
}
}
"CronList" => "Listing scheduled tasks...".to_string(),
name if name == "Agent" || name.starts_with("Agent(") => {
let task = input
.get("description")
.or_else(|| input.get("prompt"))
.and_then(|v| v.as_str());
let agent_type = input.get("subagent_type").and_then(|v| v.as_str());
match (task, agent_type) {
(Some(t), Some(a)) => format!("Launching {} agent: {}", a, t),
(Some(t), None) => format!("Launching agent: {}", t),
(None, Some(a)) => format!("Launching {} agent...", a),
(None, None) => "Launching agent...".to_string(),
}
}
_ => format!("Using tool: {}", name),
}
}
@@ -2160,6 +2191,24 @@ mod tests {
));
}
#[test]
fn test_get_tool_state_agent() {
// CLI v2.1.69+ renamed Task to Agent
assert!(matches!(get_tool_state("Agent"), CharacterState::Thinking));
assert!(matches!(
get_tool_state("Agent(Explore)"),
CharacterState::Thinking
));
assert!(matches!(
get_tool_state("Agent(Plan)"),
CharacterState::Thinking
));
assert!(matches!(
get_tool_state("Agent(general-purpose)"),
CharacterState::Thinking
));
}
#[test]
fn test_get_tool_state_unknown() {
assert!(matches!(
@@ -2289,6 +2338,41 @@ mod tests {
assert_eq!(desc, "Listing scheduled tasks...");
}
#[test]
fn test_format_tool_description_agent_with_type_and_description() {
let input = serde_json::json!({"subagent_type": "general-purpose", "description": "Fetch user info"});
let desc = format_tool_description("Agent", &input);
assert_eq!(desc, "Launching general-purpose agent: Fetch user info");
}
#[test]
fn test_format_tool_description_agent_with_prompt() {
let input = serde_json::json!({"subagent_type": "Explore", "prompt": "Look at the repo"});
let desc = format_tool_description("Agent", &input);
assert_eq!(desc, "Launching Explore agent: Look at the repo");
}
#[test]
fn test_format_tool_description_agent_no_description() {
let input = serde_json::json!({"subagent_type": "Plan"});
let desc = format_tool_description("Agent", &input);
assert_eq!(desc, "Launching Plan agent...");
}
#[test]
fn test_format_tool_description_agent_no_fields() {
let input = serde_json::json!({});
let desc = format_tool_description("Agent", &input);
assert_eq!(desc, "Launching agent...");
}
#[test]
fn test_format_tool_description_agent_with_parenthesized_type() {
let input = serde_json::json!({"description": "Check files"});
let desc = format_tool_description("Agent(Explore)", &input);
assert_eq!(desc, "Launching agent: Check files");
}
#[test]
fn test_format_tool_description_memory_read() {
let input =
@@ -2415,6 +2499,7 @@ mod tests {
assert!(result.is_some());
let data = result.unwrap();
assert_eq!(data.agent_id, "agent-abc123");
assert_eq!(data.agent_type, None);
assert_eq!(data.parent_tool_use_id, Some("toolu_01XYZ789".to_string()));
}
@@ -2426,6 +2511,7 @@ mod tests {
assert!(result.is_some());
let data = result.unwrap();
assert_eq!(data.agent_id, "agent-xyz789");
assert_eq!(data.agent_type, None);
assert_eq!(data.parent_tool_use_id, None);
}
@@ -2445,9 +2531,34 @@ mod tests {
assert!(result.is_some());
let data = result.unwrap();
assert_eq!(data.agent_id, "agent-test");
assert_eq!(data.agent_type, None);
assert_eq!(data.parent_tool_use_id, Some("toolu_test".to_string()));
}
#[test]
fn test_parse_subagent_start_hook_with_agent_type() {
let line = r#"[SubagentStart Hook] agent_id=agent-abc123, agent_type=general-purpose, parent_tool_use_id=Some("toolu_01XYZ789"), session_id=123"#;
let result = parse_subagent_start_hook(line);
assert!(result.is_some());
let data = result.unwrap();
assert_eq!(data.agent_id, "agent-abc123");
assert_eq!(data.agent_type, Some("general-purpose".to_string()));
assert_eq!(data.parent_tool_use_id, Some("toolu_01XYZ789".to_string()));
}
#[test]
fn test_parse_subagent_start_hook_with_explore_type() {
let line = r#"[SubagentStart Hook] agent_id=agent-xyz789, agent_type=Explore, parent_tool_use_id=None, session_id=456"#;
let result = parse_subagent_start_hook(line);
assert!(result.is_some());
let data = result.unwrap();
assert_eq!(data.agent_id, "agent-xyz789");
assert_eq!(data.agent_type, Some("Explore".to_string()));
assert_eq!(data.parent_tool_use_id, None);
}
// SubagentStop hook parsing tests
#[test]
fn test_parse_subagent_stop_hook_with_parent() {