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={:?}", tracing::debug!("Parsed SubagentStart hook: agent_id={}, parent={:?}",
agent_data.agent_id, agent_data.parent_tool_use_id); 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( let _ = app.emit(
"claude:agent-update", "claude:agent-update",
serde_json::json!({ serde_json::json!({
"conversationId": conversation_id.clone(), "conversationId": conversation_id.clone(),
"toolUseId": agent_data.parent_tool_use_id, "toolUseId": agent_data.parent_tool_use_id,
"agentId": agent_data.agent_id, "agentId": agent_data.agent_id,
"agentType": agent_data.agent_type,
}), }),
); );
} }
@@ -986,11 +987,12 @@ fn handle_stderr(
#[derive(Debug)] #[derive(Debug)]
struct SubagentStartData { struct SubagentStartData {
agent_id: String, agent_id: String,
agent_type: Option<String>,
parent_tool_use_id: Option<String>, parent_tool_use_id: Option<String>,
} }
fn parse_subagent_start_hook(line: &str) -> Option<SubagentStartData> { 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 // Extract agent_id
let agent_id = line let agent_id = line
@@ -1001,6 +1003,15 @@ fn parse_subagent_start_hook(line: &str) -> Option<SubagentStartData> {
.trim() .trim()
.to_string(); .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 // Extract parent_tool_use_id if present
let parent_tool_use_id = if line.contains("parent_tool_use_id=Some") { let parent_tool_use_id = if line.contains("parent_tool_use_id=Some") {
line.split("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 { Some(SubagentStartData {
agent_id, agent_id,
agent_type,
parent_tool_use_id, parent_tool_use_id,
}) })
} }
@@ -1272,11 +1284,15 @@ fn process_json_line(
} }
} }
// Emit agent-start event for Task tool invocations // Emit agent-start event for Task/Agent tool invocations
// Support both "Task" and "Task(agent_type)" syntax (CLI v2.1.33+) // Support "Task"/"Task(agent_type)" (CLI v2.1.33+) and
if name == "Task" || name.starts_with("Task(") { // "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 let description = input
.get("description") .get("description")
.or_else(|| input.get("prompt"))
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("Subagent") .unwrap_or("Subagent")
.to_string(); .to_string();
@@ -1943,7 +1959,9 @@ fn get_tool_state(tool_name: &str) -> CharacterState {
CharacterState::Coding CharacterState::Coding
} else if tool_name.starts_with("mcp__") { } else if tool_name.starts_with("mcp__") {
CharacterState::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 CharacterState::Thinking
} else { } else {
CharacterState::Typing CharacterState::Typing
@@ -2067,6 +2085,19 @@ fn format_tool_description(name: &str, input: &serde_json::Value) -> String {
} }
} }
"CronList" => "Listing scheduled tasks...".to_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), _ => 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] #[test]
fn test_get_tool_state_unknown() { fn test_get_tool_state_unknown() {
assert!(matches!( assert!(matches!(
@@ -2289,6 +2338,41 @@ mod tests {
assert_eq!(desc, "Listing scheduled tasks..."); 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] #[test]
fn test_format_tool_description_memory_read() { fn test_format_tool_description_memory_read() {
let input = let input =
@@ -2415,6 +2499,7 @@ mod tests {
assert!(result.is_some()); assert!(result.is_some());
let data = result.unwrap(); let data = result.unwrap();
assert_eq!(data.agent_id, "agent-abc123"); 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())); assert_eq!(data.parent_tool_use_id, Some("toolu_01XYZ789".to_string()));
} }
@@ -2426,6 +2511,7 @@ mod tests {
assert!(result.is_some()); assert!(result.is_some());
let data = result.unwrap(); let data = result.unwrap();
assert_eq!(data.agent_id, "agent-xyz789"); assert_eq!(data.agent_id, "agent-xyz789");
assert_eq!(data.agent_type, None);
assert_eq!(data.parent_tool_use_id, None); assert_eq!(data.parent_tool_use_id, None);
} }
@@ -2445,9 +2531,34 @@ mod tests {
assert!(result.is_some()); assert!(result.is_some());
let data = result.unwrap(); let data = result.unwrap();
assert_eq!(data.agent_id, "agent-test"); 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())); 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 // SubagentStop hook parsing tests
#[test] #[test]
fn test_parse_subagent_stop_hook_with_parent() { fn test_parse_subagent_stop_hook_with_parent() {
+2 -1
View File
@@ -282,8 +282,9 @@
class="px-1.5 py-0.5 text-[10px] rounded border {getStatusBadgeClass( class="px-1.5 py-0.5 text-[10px] rounded border {getStatusBadgeClass(
agent.status agent.status
)}" )}"
title={agent.agentId ? `ID: ${agent.agentId}` : undefined}
> >
{getSubagentTypeLabel(agent.subagentType)} {getSubagentTypeLabel(agent.agentType ?? agent.subagentType)}
</span> </span>
</div> </div>
<span <span
+22
View File
@@ -121,6 +121,28 @@ describe("agents store", () => {
const agents = get(getAgentsForConversation(conversationId)); const agents = get(getAgentsForConversation(conversationId));
expect(agents[0].agentId).toBeUndefined(); expect(agents[0].agentId).toBeUndefined();
}); });
it("updates agentType when provided alongside agentId", () => {
const agent = createMockAgent({ agentId: undefined });
agentStore.addAgent(conversationId, agent);
agentStore.updateAgentId(conversationId, agent.toolUseId, "agent-abc123", "general-purpose");
const agents = get(getAgentsForConversation(conversationId));
expect(agents[0].agentId).toBe("agent-abc123");
expect(agents[0].agentType).toBe("general-purpose");
});
it("does not set agentType when not provided", () => {
const agent = createMockAgent({ agentId: undefined });
agentStore.addAgent(conversationId, agent);
agentStore.updateAgentId(conversationId, agent.toolUseId, "agent-abc123");
const agents = get(getAgentsForConversation(conversationId));
expect(agents[0].agentId).toBe("agent-abc123");
expect(agents[0].agentType).toBeUndefined();
});
}); });
describe("endAgent", () => { describe("endAgent", () => {
+2 -1
View File
@@ -24,7 +24,7 @@ function createAgentStore() {
}); });
}, },
updateAgentId(conversationId: string, toolUseId: string, agentId: string) { updateAgentId(conversationId: string, toolUseId: string, agentId: string, agentType?: string) {
agentsByConversation.update((state) => { agentsByConversation.update((state) => {
const agents = state[conversationId]; const agents = state[conversationId];
if (!agents) return state; if (!agents) return state;
@@ -36,6 +36,7 @@ function createAgentStore() {
updated[agentIndex] = { updated[agentIndex] = {
...updated[agentIndex], ...updated[agentIndex],
agentId, agentId,
...(agentType !== undefined ? { agentType } : {}),
}; };
return { return {
+3 -2
View File
@@ -538,9 +538,10 @@ export async function initializeTauriListeners() {
conversationId: string; conversationId: string;
toolUseId: string; toolUseId: string;
agentId: string; agentId: string;
agentType?: string;
}>("claude:agent-update", (event) => { }>("claude:agent-update", (event) => {
const { conversationId, toolUseId, agentId } = event.payload; const { conversationId, toolUseId, agentId, agentType } = event.payload;
agentStore.updateAgentId(conversationId, toolUseId, agentId); agentStore.updateAgentId(conversationId, toolUseId, agentId, agentType);
}); });
unlisteners.push(agentUpdateUnlisten); unlisteners.push(agentUpdateUnlisten);
+1
View File
@@ -3,6 +3,7 @@ export type AgentStatus = "running" | "completed" | "errored";
export interface AgentInfo { export interface AgentInfo {
toolUseId: string; toolUseId: string;
agentId?: string; agentId?: string;
agentType?: string;
description: string; description: string;
subagentType: string; subagentType: string;
startedAt: number; startedAt: number;