Skip to content

Commit 60befd0

Browse files
echobtfactorydroid
andauthored
fix(tui): improve subagent display format with Factory-style todos (#274)
- Change subagent display from spinner with status to cleaner Factory-style format: ● Task {agent_type} ⎿ [pending] task1 [in_progress] task2 [completed] task3 - Fix duplicate display bug: Task tools are no longer added to both tool_calls and active_subagents. They now only appear as subagent tasks with proper todo tracking. - Remove animated spinner for subagents, use solid dot indicator instead - Show current activity with ⎿ prefix when no todos are available yet Co-authored-by: Droid Agent <droid@factory.ai>
1 parent 3c34732 commit 60befd0

2 files changed

Lines changed: 48 additions & 80 deletions

File tree

cortex-tui/src/runner/event_loop.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2928,9 +2928,12 @@ impl EventLoop {
29282928
arguments: arguments.clone(),
29292929
});
29302930

2931-
// Add tool call to display
2932-
self.app_state
2933-
.add_tool_call(id.clone(), name.clone(), arguments.clone());
2931+
// Add tool call to display - but NOT for Task tools
2932+
// Task tools use SubagentTaskDisplay for better visualization
2933+
if name != "Task" && name != "task" {
2934+
self.app_state
2935+
.add_tool_call(id.clone(), name.clone(), arguments.clone());
2936+
}
29342937

29352938
// Special handling for Questions tool - show interactive TUI
29362939
if name == "Questions" || name == "question" {
@@ -2945,7 +2948,11 @@ impl EventLoop {
29452948

29462949
// Special handling for Task and Batch tools - use UnifiedToolExecutor
29472950
if name == "Task" || name == "task" || name == "Batch" || name == "batch" {
2948-
self.app_state.update_tool_status(&id, ToolStatus::Running);
2951+
// Note: Task tools don't need update_tool_status here because
2952+
// they use SubagentTaskDisplay which has its own status tracking
2953+
if name == "Batch" || name == "batch" {
2954+
self.app_state.update_tool_status(&id, ToolStatus::Running);
2955+
}
29492956

29502957
// Use UnifiedToolExecutor if available, otherwise fall back to old behavior
29512958
if self.unified_executor.is_some() {

cortex-tui/src/views/minimal_session.rs

Lines changed: 37 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -379,112 +379,73 @@ impl<'a> MinimalSessionView<'a> {
379379
lines
380380
}
381381

382-
/// Renders a subagent task with animated spinner and progress
382+
/// Renders a subagent task with todos in Factory-style format
383+
///
384+
/// Format:
385+
/// ```
386+
/// ● Task {agent_type}
387+
/// ⎿ [pending] task1
388+
/// [in_progress] task2
389+
/// [completed] task3
390+
/// ```
383391
fn render_subagent(&self, task: &SubagentTaskDisplay) -> Vec<Line<'static>> {
384-
use crate::ui::consts::TOOL_SPINNER_FRAMES;
392+
use crate::app::SubagentTodoStatus;
385393
let mut lines = Vec::new();
386394

387-
// Status indicator with color - animated spinner for active status
395+
// Status indicator with color
388396
let (indicator, indicator_color) = match &task.status {
389-
SubagentDisplayStatus::Starting => {
390-
let frame = TOOL_SPINNER_FRAMES[task.spinner_frame % TOOL_SPINNER_FRAMES.len()];
391-
(frame.to_string(), self.colors.warning)
392-
}
393-
SubagentDisplayStatus::Thinking => {
394-
let frame = TOOL_SPINNER_FRAMES[task.spinner_frame % TOOL_SPINNER_FRAMES.len()];
395-
(frame.to_string(), self.colors.accent)
396-
}
397-
SubagentDisplayStatus::ExecutingTool(_) => {
398-
let frame = TOOL_SPINNER_FRAMES[task.spinner_frame % TOOL_SPINNER_FRAMES.len()];
399-
(frame.to_string(), Color::Blue)
400-
}
401-
SubagentDisplayStatus::Completed => ("✓".to_string(), self.colors.success),
402-
SubagentDisplayStatus::Failed => ("✗".to_string(), self.colors.error),
397+
SubagentDisplayStatus::Starting
398+
| SubagentDisplayStatus::Thinking
399+
| SubagentDisplayStatus::ExecutingTool(_) => ("●", self.colors.accent),
400+
SubagentDisplayStatus::Completed => ("●", self.colors.success),
401+
SubagentDisplayStatus::Failed => ("●", self.colors.error),
403402
};
404403

405-
// Line 1: ◐ [agent_type] description - current_activity (Xs)
406-
let elapsed_secs = task.elapsed().as_secs();
404+
// Line 1: ● Task {agent_type}
407405
lines.push(Line::from(vec![
408406
Span::styled(indicator, Style::default().fg(indicator_color)),
409407
Span::raw(" "),
410408
Span::styled(
411-
format!("[{}]", task.agent_type),
412-
Style::default().fg(self.colors.text_muted),
413-
),
414-
Span::raw(" "),
415-
Span::styled(
416-
task.description.clone(),
409+
format!("Task {}", task.agent_type),
417410
Style::default()
418411
.fg(self.colors.accent)
419412
.add_modifier(Modifier::BOLD),
420413
),
421-
Span::styled(
422-
format!(" - {}", task.current_activity),
423-
Style::default().fg(self.colors.text_dim),
424-
),
425-
Span::styled(
426-
format!(" ({}s)", elapsed_secs),
427-
Style::default().fg(self.colors.text_muted),
428-
),
429414
]));
430415

431-
// Display todos if any (real-time todo list from subagent)
416+
// Display todos if any - use ⎿ prefix for first, space for rest
432417
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),
418+
for (i, todo) in task.todos.iter().enumerate() {
419+
let (status_text, status_color) = match todo.status {
420+
SubagentTodoStatus::Completed => ("[completed]", self.colors.success),
421+
SubagentTodoStatus::InProgress => ("[in_progress]", self.colors.accent),
422+
SubagentTodoStatus::Pending => ("[pending]", self.colors.text_muted),
439423
};
440424
// Truncate long todo content
441-
let content = if todo.content.len() > 55 {
442-
format!("{}...", &todo.content.chars().take(52).collect::<String>())
425+
let content = if todo.content.len() > 50 {
426+
format!("{}...", &todo.content.chars().take(47).collect::<String>())
443427
} else {
444428
todo.content.clone()
445429
};
430+
// First line uses ⎿, rest use indentation
431+
let prefix = if i == 0 { " ⎿ " } else { " " };
446432
lines.push(Line::from(vec![
447-
Span::styled(" ", Style::default()),
448-
Span::styled(status_icon, Style::default().fg(status_color)),
433+
Span::styled(prefix, Style::default().fg(self.colors.text_muted)),
434+
Span::styled(status_text, Style::default().fg(status_color)),
449435
Span::styled(" ", Style::default()),
450436
Span::styled(content, Style::default().fg(self.colors.text_dim)),
451437
]));
452438
}
453-
} else if !task.tool_calls.is_empty() {
454-
// Fallback: show tool calls summary if no todos
455-
let tool_summary: String = task
456-
.tool_calls
457-
.iter()
458-
.map(|(name, success)| {
459-
if *success {
460-
format!("✓{}", name)
461-
} else {
462-
format!("✗{}", name)
463-
}
464-
})
465-
.collect::<Vec<_>>()
466-
.join(" ");
467-
468-
lines.push(Line::from(vec![
469-
Span::styled(" │ ", Style::default().fg(self.colors.text_muted)),
470-
Span::styled(
471-
format!("Tools: {}", tool_summary),
472-
Style::default().fg(self.colors.text_dim),
473-
),
474-
]));
475-
} else if !task.output_preview.is_empty() {
476-
// Fallback: show output preview if no todos and no tool calls
477-
let preview = if task.output_preview.len() > 60 {
478-
format!(
479-
"{}...",
480-
&task.output_preview.chars().take(57).collect::<String>()
481-
)
439+
} else {
440+
// No todos yet - show current activity with ⎿
441+
let activity = if task.current_activity.is_empty() {
442+
"Initializing...".to_string()
482443
} else {
483-
task.output_preview.clone()
444+
task.current_activity.clone()
484445
};
485446
lines.push(Line::from(vec![
486-
Span::styled(" ", Style::default().fg(self.colors.text_muted)),
487-
Span::styled(preview, Style::default().fg(self.colors.text_dim)),
447+
Span::styled(" ", Style::default().fg(self.colors.text_muted)),
448+
Span::styled(activity, Style::default().fg(self.colors.text_dim)),
488449
]));
489450
}
490451

0 commit comments

Comments
 (0)