Skip to content

Commit ff00fc2

Browse files
echobtfactorydroid
andauthored
feat(tui): display real-time todos for subagents (#259)
- Add SubagentTodoItem and SubagentTodoStatus types to app.rs - Add todos field to SubagentTaskDisplay struct - Add TodoUpdated progress event for subagent todo updates - Handle TodoUpdated event in event_loop to update subagent todos - Render todos in render_subagent with status icons (✓/●/○) - Remove '── Subagents ──' header for cleaner display This allows the TUI to display the subagent's current todo list in real-time instead of just showing line counts or output previews. Co-authored-by: Droid Agent <droid@factory.ai>
1 parent 093bfb8 commit ff00fc2

4 files changed

Lines changed: 116 additions & 18 deletions

File tree

cortex-engine/src/tools/handlers/subagent/progress.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ pub enum ProgressEvent {
105105

106106
/// Warning message.
107107
Warning { session_id: String, message: String },
108+
109+
/// Todo list updated (from TodoWrite tool).
110+
TodoUpdated {
111+
session_id: String,
112+
/// Todo items: (content, status) where status is "pending", "in_progress", or "completed"
113+
todos: Vec<(String, String)>,
114+
},
108115
}
109116

110117
impl ProgressEvent {
@@ -125,6 +132,7 @@ impl ProgressEvent {
125132
Self::Cancelled { session_id, .. } => session_id,
126133
Self::Info { session_id, .. } => session_id,
127134
Self::Warning { session_id, .. } => session_id,
135+
Self::TodoUpdated { session_id, .. } => session_id,
128136
}
129137
}
130138

@@ -216,6 +224,22 @@ impl ProgressEvent {
216224
Self::Warning { message, .. } => {
217225
format!("Warning: {}", message)
218226
}
227+
Self::TodoUpdated { todos, .. } => {
228+
let in_progress = todos
229+
.iter()
230+
.filter(|(_, status)| status == "in_progress")
231+
.count();
232+
let completed = todos
233+
.iter()
234+
.filter(|(_, status)| status == "completed")
235+
.count();
236+
format!(
237+
"Todos: {} items ({} in progress, {} completed)",
238+
todos.len(),
239+
in_progress,
240+
completed
241+
)
242+
}
219243
}
220244
}
221245
}
@@ -454,6 +478,14 @@ impl SubagentProgress {
454478
});
455479
}
456480

481+
/// Update todo list (from TodoWrite tool).
482+
pub fn update_todos(&self, todos: Vec<(String, String)>) {
483+
let _ = self.event_tx.send(ProgressEvent::TodoUpdated {
484+
session_id: self.session_id.clone(),
485+
todos,
486+
});
487+
}
488+
457489
/// Get summary statistics.
458490
pub fn stats(&self) -> ProgressStats {
459491
ProgressStats {

cortex-tui/src/app.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,36 @@ pub struct PendingToolResult {
179179
// SUBAGENT TASK DISPLAY
180180
// ============================================================================
181181

182+
/// A simple todo item for display in the subagent task view.
183+
#[derive(Debug, Clone, PartialEq, Eq)]
184+
pub struct SubagentTodoItem {
185+
/// Todo item content/description.
186+
pub content: String,
187+
/// Status: pending, in_progress, or completed.
188+
pub status: SubagentTodoStatus,
189+
}
190+
191+
/// Status of a subagent todo item.
192+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193+
pub enum SubagentTodoStatus {
194+
/// Not started yet.
195+
Pending,
196+
/// Currently being worked on.
197+
InProgress,
198+
/// Completed.
199+
Completed,
200+
}
201+
202+
impl SubagentTodoItem {
203+
/// Create a new todo item.
204+
pub fn new(content: impl Into<String>, status: SubagentTodoStatus) -> Self {
205+
Self {
206+
content: content.into(),
207+
status,
208+
}
209+
}
210+
}
211+
182212
/// Display state for an active subagent task.
183213
#[derive(Debug, Clone)]
184214
pub struct SubagentTaskDisplay {
@@ -202,6 +232,8 @@ pub struct SubagentTaskDisplay {
202232
pub output_preview: String,
203233
/// Start time.
204234
pub started_at: Instant,
235+
/// Current todo items from the subagent (if any).
236+
pub todos: Vec<SubagentTodoItem>,
205237
}
206238

207239
impl SubagentTaskDisplay {
@@ -223,6 +255,7 @@ impl SubagentTaskDisplay {
223255
tool_calls: Vec::new(),
224256
output_preview: String::new(),
225257
started_at: Instant::now(),
258+
todos: Vec::new(),
226259
}
227260
}
228261

cortex-tui/src/runner/event_loop.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3384,6 +3384,28 @@ impl EventLoop {
33843384
});
33853385
}
33863386

3387+
ProgressEvent::TodoUpdated { todos, .. } => {
3388+
use crate::app::{SubagentTodoItem, SubagentTodoStatus};
3389+
tracing::debug!(
3390+
"Subagent todos updated: {} - {} items",
3391+
session_id,
3392+
todos.len()
3393+
);
3394+
self.app_state.update_subagent(&session_id, |task| {
3395+
task.todos = todos
3396+
.iter()
3397+
.map(|(content, status)| {
3398+
let todo_status = match status.as_str() {
3399+
"completed" => SubagentTodoStatus::Completed,
3400+
"in_progress" => SubagentTodoStatus::InProgress,
3401+
_ => SubagentTodoStatus::Pending,
3402+
};
3403+
SubagentTodoItem::new(content.clone(), todo_status)
3404+
})
3405+
.collect();
3406+
});
3407+
}
3408+
33873409
_ => {}
33883410
}
33893411
}

cortex-tui/src/views/minimal_session.rs

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -428,8 +428,30 @@ impl<'a> MinimalSessionView<'a> {
428428
),
429429
]));
430430

431-
// Line 2: Tool calls summary if any
432-
if !task.tool_calls.is_empty() {
431+
// Display todos if any (real-time todo list from subagent)
432+
if !task.todos.is_empty() {
433+
use crate::app::SubagentTodoStatus;
434+
for todo in &task.todos {
435+
let (status_icon, status_color) = match todo.status {
436+
SubagentTodoStatus::Completed => ("✓", self.colors.success),
437+
SubagentTodoStatus::InProgress => ("●", self.colors.accent),
438+
SubagentTodoStatus::Pending => ("○", self.colors.text_muted),
439+
};
440+
// Truncate long todo content
441+
let content = if todo.content.len() > 55 {
442+
format!("{}...", &todo.content.chars().take(52).collect::<String>())
443+
} else {
444+
todo.content.clone()
445+
};
446+
lines.push(Line::from(vec![
447+
Span::styled(" ", Style::default()),
448+
Span::styled(status_icon, Style::default().fg(status_color)),
449+
Span::styled(" ", Style::default()),
450+
Span::styled(content, Style::default().fg(self.colors.text_dim)),
451+
]));
452+
}
453+
} else if !task.tool_calls.is_empty() {
454+
// Fallback: show tool calls summary if no todos
433455
let tool_summary: String = task
434456
.tool_calls
435457
.iter()
@@ -450,10 +472,8 @@ impl<'a> MinimalSessionView<'a> {
450472
Style::default().fg(self.colors.text_dim),
451473
),
452474
]));
453-
}
454-
455-
// Line 3: Output preview if any
456-
if !task.output_preview.is_empty() {
475+
} else if !task.output_preview.is_empty() {
476+
// Fallback: show output preview if no todos and no tool calls
457477
let preview = if task.output_preview.len() > 60 {
458478
format!(
459479
"{}...",
@@ -587,18 +607,9 @@ impl<'a> MinimalSessionView<'a> {
587607
}
588608

589609
// Render active subagents (always visible when running)
590-
if !self.app_state.active_subagents.is_empty() {
591-
// Add separator if we have other content
592-
if !all_lines.is_empty() {
593-
all_lines.push(Line::from(Span::styled(
594-
"── Subagents ──",
595-
Style::default().fg(self.colors.text_muted),
596-
)));
597-
}
598-
599-
for task in &self.app_state.active_subagents {
600-
all_lines.extend(self.render_subagent(task));
601-
}
610+
// Note: No "Subagents" header - todos are displayed directly
611+
for task in &self.app_state.active_subagents {
612+
all_lines.extend(self.render_subagent(task));
602613
}
603614

604615
let total_lines = all_lines.len();

0 commit comments

Comments
 (0)