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={:?}",
|
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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user