Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cortex-tui/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_<tool_call_id>")
session_id: String,
/// Todo items: (content, status) where status is "pending", "in_progress", or "completed"
todos: Vec<(String, String)>,
},
}

/// Events from subagent executions.
Expand Down
81 changes: 80 additions & 1 deletion cortex-tui/src/runner/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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);
Expand Down Expand Up @@ -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();
}
});
}
}
}

Expand Down
Loading