diff --git a/cortex-tui/src/events.rs b/cortex-tui/src/events.rs index 989269da..a8476a6a 100644 --- a/cortex-tui/src/events.rs +++ b/cortex-tui/src/events.rs @@ -59,6 +59,14 @@ pub enum ToolEvent { error: String, duration: std::time::Duration, }, + /// Subagent todo list was updated (from TodoWrite tool). + /// Used to update the UI's SubagentTaskDisplay with real-time todo progress. + TodoUpdated { + /// Session ID of the subagent (e.g., "subagent_") + session_id: String, + /// Todo items: (content, status) where status is "pending", "in_progress", or "completed" + todos: Vec<(String, String)>, + }, } /// Events from subagent executions. diff --git a/cortex-tui/src/runner/event_loop.rs b/cortex-tui/src/runner/event_loop.rs index 422a6a69..00896003 100644 --- a/cortex-tui/src/runner/event_loop.rs +++ b/cortex-tui/src/runner/event_loop.rs @@ -753,9 +753,38 @@ impl EventLoop { ) .collect(); + // Maximum size for tool result output to prevent HTTP 413 Payload Too Large + const MAX_TOOL_OUTPUT_SIZE: usize = 32_000; // 32KB per tool result + for (tc_id, tc_name, tc_args) in &iteration_tool_calls { tracing::info!("Subagent executing tool: {} ({})", tc_name, tc_id); + // If this is TodoWrite, parse and send todo updates to UI + if tc_name == "TodoWrite" { + if let Some(todos_arr) = tc_args.get("todos").and_then(|v| v.as_array()) { + let todos: Vec<(String, String)> = todos_arr + .iter() + .filter_map(|t| { + let content = t.get("content").and_then(|v| v.as_str())?; + let status = t + .get("status") + .and_then(|v| v.as_str()) + .unwrap_or("pending"); + Some((content.to_string(), status.to_string())) + }) + .collect(); + + if !todos.is_empty() { + let _ = tool_tx + .send(ToolEvent::TodoUpdated { + session_id: format!("subagent_{}", id), + todos, + }) + .await; + } + } + } + let result = registry.execute(tc_name, tc_args.clone()).await; match result { Ok(tool_result) => { @@ -765,7 +794,20 @@ impl EventLoop { "failed" }; tool_calls_executed.push(format!("{}: {}", tc_name, status)); - tool_results.push((tc_id.clone(), tool_result.output)); + + // Truncate large tool outputs to prevent payload too large errors + let output = if tool_result.output.len() > MAX_TOOL_OUTPUT_SIZE { + let truncated = &tool_result.output[..MAX_TOOL_OUTPUT_SIZE]; + format!( + "{}...\n\n[Output truncated: {} bytes total, showing first {} bytes]", + truncated, + tool_result.output.len(), + MAX_TOOL_OUTPUT_SIZE + ) + } else { + tool_result.output + }; + tool_results.push((tc_id.clone(), output)); } Err(e) => { let error_msg = format!("Error executing {}: {}", tc_name, e); @@ -3384,6 +3426,43 @@ impl EventLoop { } } } + + ToolEvent::TodoUpdated { session_id, todos } => { + // Update the subagent's todo list in the UI + use crate::app::{SubagentTodoItem, SubagentTodoStatus}; + + tracing::debug!( + "Subagent todo list updated: {} ({} items)", + session_id, + todos.len() + ); + + self.app_state.update_subagent(&session_id, |task| { + task.todos = todos + .iter() + .map(|(content, status)| { + let status = match status.as_str() { + "in_progress" => SubagentTodoStatus::InProgress, + "completed" => SubagentTodoStatus::Completed, + _ => SubagentTodoStatus::Pending, + }; + SubagentTodoItem { + content: content.clone(), + status, + } + }) + .collect(); + + // Update activity based on in-progress item + if let Some(in_progress) = task + .todos + .iter() + .find(|t| matches!(t.status, SubagentTodoStatus::InProgress)) + { + task.current_activity = in_progress.content.clone(); + } + }); + } } }