From eb7db1fb2f94212ea4351d8314a13f2b34e55487 Mon Sep 17 00:00:00 2001 From: Droid Agent Date: Tue, 27 Jan 2026 14:52:00 +0000 Subject: [PATCH] fix(tui): display subagent todos and prevent HTTP 413 payload errors This commit addresses two issues when running tasks via the Task tool: 1. Todo list display: When a subagent calls TodoWrite, the todos are now parsed and forwarded to the UI via a new ToolEvent::TodoUpdated event. The SubagentTaskDisplay in the TUI is then updated to show the todos with their status (pending, in_progress, completed). 2. HTTP 413 Payload Too Large: Tool outputs from subagent tool calls are now truncated to 32KB to prevent the cumulative context from exceeding backend payload limits. This is especially important for tools like Grep or Read that can return large outputs. Changes: - Add ToolEvent::TodoUpdated variant to events.rs - Detect TodoWrite calls in spawn_subagent and send todo updates to UI - Truncate tool outputs > 32KB with a clear truncation message - Handle TodoUpdated event in handle_tool_event to update app_state --- cortex-tui/src/events.rs | 8 +++ cortex-tui/src/runner/event_loop.rs | 81 ++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 1 deletion(-) 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(); + } + }); + } } }