From 021269983d96c399f5c3d8f5ab5da8bdd5057d60 Mon Sep 17 00:00:00 2001 From: Hikari Date: Wed, 11 Mar 2026 11:21:43 -0700 Subject: [PATCH] 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 --- src-tauri/src/wsl_bridge.rs | 123 +++++++++++++++++++- src/lib/components/AgentMonitorPanel.svelte | 3 +- src/lib/stores/agents.test.ts | 22 ++++ src/lib/stores/agents.ts | 3 +- src/lib/tauri.ts | 5 +- src/lib/types/agents.ts | 1 + 6 files changed, 147 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs index 4220f11..7fffbda 100644 --- a/src-tauri/src/wsl_bridge.rs +++ b/src-tauri/src/wsl_bridge.rs @@ -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, parent_tool_use_id: Option, } fn parse_subagent_start_hook(line: &str) -> Option { - // 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 { .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 { 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() { diff --git a/src/lib/components/AgentMonitorPanel.svelte b/src/lib/components/AgentMonitorPanel.svelte index 6efc412..dd30635 100644 --- a/src/lib/components/AgentMonitorPanel.svelte +++ b/src/lib/components/AgentMonitorPanel.svelte @@ -282,8 +282,9 @@ class="px-1.5 py-0.5 text-[10px] rounded border {getStatusBadgeClass( agent.status )}" + title={agent.agentId ? `ID: ${agent.agentId}` : undefined} > - {getSubagentTypeLabel(agent.subagentType)} + {getSubagentTypeLabel(agent.agentType ?? agent.subagentType)} { const agents = get(getAgentsForConversation(conversationId)); 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", () => { diff --git a/src/lib/stores/agents.ts b/src/lib/stores/agents.ts index 4ea5251..abdd57e 100644 --- a/src/lib/stores/agents.ts +++ b/src/lib/stores/agents.ts @@ -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) => { const agents = state[conversationId]; if (!agents) return state; @@ -36,6 +36,7 @@ function createAgentStore() { updated[agentIndex] = { ...updated[agentIndex], agentId, + ...(agentType !== undefined ? { agentType } : {}), }; return { diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 274d7f1..cb0c501 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -538,9 +538,10 @@ export async function initializeTauriListeners() { conversationId: string; toolUseId: string; agentId: string; + agentType?: string; }>("claude:agent-update", (event) => { - const { conversationId, toolUseId, agentId } = event.payload; - agentStore.updateAgentId(conversationId, toolUseId, agentId); + const { conversationId, toolUseId, agentId, agentType } = event.payload; + agentStore.updateAgentId(conversationId, toolUseId, agentId, agentType); }); unlisteners.push(agentUpdateUnlisten); diff --git a/src/lib/types/agents.ts b/src/lib/types/agents.ts index cc9e3b5..1803cbb 100644 --- a/src/lib/types/agents.ts +++ b/src/lib/types/agents.ts @@ -3,6 +3,7 @@ export type AgentStatus = "running" | "completed" | "errored"; export interface AgentInfo { toolUseId: string; agentId?: string; + agentType?: string; description: string; subagentType: string; startedAt: number;