use std::collections::HashMap;
use serde_json::Value;
use super::event_dispatch::map_todo_items;
use super::{
App, AutonomyLevel, DisplayMessage, DisplayRole, DisplayToolCall, ToolExecution, ToolState,
};
impl App {
pub(super) fn handle_tool_started(
&mut self,
tool_id: String,
tool_name: String,
args: HashMap<String, Value>,
) {
self.finalize_active_thinking();
if tool_name == "spawn_subagent" {
let all_finished = !self.state.active_subagents.is_empty()
&& self.state.active_subagents.iter().all(|s| s.finished);
if all_finished {
self.state.active_subagents.retain(|s| s.backgrounded);
}
let agent_name = args
.get("agent_type")
.and_then(|v| v.as_str())
.unwrap_or("Agent")
.to_string();
let task = args
.get("task")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let mut sa = crate::widgets::nested_tool::SubagentDisplayState::new(
String::new(), agent_name,
task,
);
sa.parent_tool_id = Some(tool_id.clone());
sa.description = args
.get("description")
.and_then(|v| v.as_str())
.map(String::from);
self.state.active_subagents.push(sa);
}
self.state.active_tools.push(ToolExecution {
id: tool_id,
name: tool_name,
output_lines: Vec::new(),
state: ToolState::Running,
elapsed_secs: 0,
started_at: std::time::Instant::now(),
tick_count: 0,
parent_id: None,
depth: 0,
args,
});
self.state.dirty = true;
self.state.message_generation += 1;
}
pub(super) fn handle_tool_output(&mut self, tool_id: String, output: String) {
if let Some(tool) = self.state.active_tools.iter_mut().find(|t| t.id == tool_id) {
tool.output_lines.push(output);
}
self.state.dirty = true;
}
pub(super) fn handle_tool_result(
&mut self,
tool_id: String,
tool_name: String,
output: String,
success: bool,
result_args: HashMap<String, Value>,
) {
let arguments = self
.state
.active_tools
.iter()
.find(|t| t.id == tool_id)
.map(|t| t.args.clone())
.unwrap_or(result_args);
let is_todo_tool = matches!(
tool_name.as_str(),
"write_todos" | "update_todo" | "complete_todo" | "list_todos" | "clear_todos"
);
let (display_lines, collapsed) = if tool_name == "ask_user" {
let question = arguments
.get("question")
.and_then(|v| v.as_str())
.unwrap_or("question");
let answer = output.strip_prefix("User answered: ").unwrap_or(&output);
(vec![format!("· {question} → {answer}")], false)
} else if is_todo_tool {
let summary =
crate::formatters::todo_formatter::summarize_todo_result(&tool_name, &output);
(vec![summary], false)
} else if tool_name == "present_plan" {
let step_count = output
.split_once(" steps)")
.and_then(|(before, _)| before.rsplit(", ").next())
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
if step_count > 0 {
(
vec![format!("Plan approved · {step_count} todos created")],
false,
)
} else {
(vec!["Plan approved".to_string()], false)
}
} else {
use crate::widgets::conversation::is_diff_tool;
let result_lines: Vec<String> =
output.lines().take(50).map(|l| l.to_string()).collect();
let lines = if result_lines.is_empty() && !output.is_empty() {
vec![output.clone()]
} else {
result_lines
};
use crate::formatters::tool_registry::{ToolCategory, categorize_tool};
let is_file_read = categorize_tool(&tool_name) == ToolCategory::FileRead;
let collapse = is_file_read || (lines.len() > 5 && !is_diff_tool(&tool_name));
(lines, collapse)
};
if tool_name == "spawn_subagent"
&& self
.state
.active_subagents
.iter()
.any(|s| s.backgrounded && s.parent_tool_id.as_deref() == Some(&tool_id))
{
self.state.active_tools.retain(|t| t.id != tool_id);
self.state.dirty = true;
return;
}
if tool_name == "spawn_subagent" {
let subagent_idx = self
.state
.active_subagents
.iter()
.position(|s| s.parent_tool_id.as_deref() == Some(&tool_id))
.or_else(|| {
let task_text = arguments.get("task").and_then(|v| v.as_str()).unwrap_or("");
self.state
.active_subagents
.iter()
.position(|s| s.task == task_text)
});
if let Some(idx) = subagent_idx {
let removed = self.state.active_subagents.remove(idx);
self.state.bg_subagent_map.remove(&removed.subagent_id);
let summary_line = removed.completion_summary();
self.state.messages.push(DisplayMessage {
role: DisplayRole::Assistant,
content: String::new(),
tool_call: Some(DisplayToolCall {
name: tool_name.clone(),
arguments,
summary: None,
success,
collapsed: false,
result_lines: vec![summary_line],
nested_calls: Vec::new(),
}),
collapsed: false,
thinking_started_at: None,
thinking_duration_secs: None,
});
}
} else if !display_lines.is_empty() {
self.state.messages.push(DisplayMessage {
role: DisplayRole::Assistant,
content: String::new(),
tool_call: Some(DisplayToolCall {
name: tool_name.clone(),
arguments,
summary: None,
success,
collapsed,
result_lines: display_lines,
nested_calls: Vec::new(),
}),
collapsed: false,
thinking_started_at: None,
thinking_duration_secs: None,
});
}
if (is_todo_tool || tool_name == "present_plan")
&& let Some(ref mgr) = self.state.todo_manager
&& let Ok(mgr) = mgr.lock()
{
self.state.todo_items = map_todo_items(&mgr);
if (tool_name == "write_todos" || tool_name == "present_plan")
&& !self.state.todo_items.is_empty()
{
self.state.todo_expanded = true;
}
if tool_name == "clear_todos" {
self.state.todo_items.clear();
}
}
self.state.dirty = true;
self.state.message_generation += 1;
}
pub(super) fn handle_tool_finished(&mut self, tool_id: String, success: bool) {
if let Some(tool) = self.state.active_tools.iter_mut().find(|t| t.id == tool_id) {
tool.state = if success {
ToolState::Completed
} else {
ToolState::Error
};
}
self.state.active_tools.retain(|t| !t.is_finished());
self.state.dirty = true;
self.state.message_generation += 1;
}
pub(super) fn handle_tool_approval_required(&mut self, description: String) {
let wd = self.state.working_dir.clone();
let _rx = self.approval_controller.start(description, wd);
self.state.dirty = true;
}
pub(super) fn handle_tool_approval_requested(
&mut self,
command: String,
working_dir: String,
response_tx: tokio::sync::oneshot::Sender<opendev_runtime::ToolApprovalDecision>,
) {
let auto_approve = match self.state.autonomy {
AutonomyLevel::Auto => true,
AutonomyLevel::SemiAuto => opendev_runtime::is_safe_command(&command),
AutonomyLevel::Manual => false,
};
if auto_approve {
let _ = response_tx.send(opendev_runtime::ToolApprovalDecision {
approved: true,
choice: "yes".to_string(),
command,
});
} else {
let _rx = self.approval_controller.start(command, working_dir);
self.approval_response_tx = Some(response_tx);
}
self.state.dirty = true;
}
pub(super) fn handle_ask_user_requested(
&mut self,
question: String,
options: Vec<String>,
default: Option<String>,
response_tx: tokio::sync::oneshot::Sender<String>,
) {
self.ask_user_controller.start(question, options, default);
self.ask_user_response_tx = Some(response_tx);
self.state.dirty = true;
}
}