generated from nhcarrigan/template
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:
+117
-6
@@ -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() {
|
||||
|
||||
@@ -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)}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
|
||||
@@ -121,6 +121,28 @@ describe("agents store", () => {
|
||||
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", () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+3
-2
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user