use super::super::*;
use a3s_tui::components::{ChecklistItem, ChecklistStatus, QueuedTask, SubagentRow};
impl App {
pub(crate) fn task_lines(&self) -> Vec<String> {
let running = self
.running_task
.as_ref()
.filter(|_| self.state != State::Idle);
if self.queue.is_empty() {
return Vec::new();
}
let theme = agent_chrome_theme();
let chrome = agent_chrome(&theme);
let queued = self
.queue
.iter()
.map(|item| {
chrome
.queued_task(item.text.clone())
.priority(i32::from(item.prio))
.sequence(item.seq)
})
.collect::<Vec<_>>();
task_queue_lines(
self.completed,
running.map(String::as_str),
queued,
self.width as usize,
)
}
pub(crate) fn viewport_rows(&self) -> usize {
let n = (self.task_lines().len() + self.plan_lines().len() + self.subagent_lines().len())
as u16;
self.height.saturating_sub(6 + self.input_height() + n) as usize
}
pub(crate) fn relayout(&mut self) {
self.viewport
.resize(self.width.saturating_sub(1), self.viewport_rows() as u16);
}
pub(crate) fn set_plan(&mut self, tasks: &[a3s_code_core::planning::Task]) {
self.plan = tasks
.iter()
.map(|t| {
let (g, c) = task_status_style(t.status);
(t.id.clone(), t.content.clone(), g, c)
})
.collect();
self.relayout();
}
pub(crate) fn set_task_status(&mut self, id: &str, glyph: char, color: Color) {
if let Some(t) = self.plan.iter_mut().find(|t| t.0 == id) {
t.2 = glyph;
t.3 = color;
}
}
pub(crate) fn plan_lines(&self) -> Vec<String> {
if self.plan.is_empty() {
return Vec::new();
}
let width = self.width as usize;
plan_checklist_lines(&self.plan, width)
}
pub(crate) fn subagent_lines(&self) -> Vec<String> {
let subagents = self.runtime.subagents();
if subagents.is_empty() {
return Vec::new();
}
let width = self.width as usize;
let task = self
.runtime
.subagent_task()
.or(self.running_task.as_deref())
.unwrap_or("parallel agents")
.trim();
let theme = agent_chrome_theme();
let chrome = agent_chrome(&theme);
let rows = subagents
.into_iter()
.map(|s| {
let elapsed = s
.ended
.unwrap_or_else(Instant::now)
.saturating_duration_since(s.started);
let mut row = chrome
.subagent_row(s.agent.clone(), s.description.clone())
.elapsed(fmt_elapsed(elapsed))
.tokens(s.tokens);
if s.done {
row = row.done(s.success.unwrap_or(true));
}
row
})
.collect::<Vec<_>>();
subagent_tracker_lines(task, rows, width)
}
}
fn subagent_tracker_lines(task: &str, rows: Vec<SubagentRow>, width: usize) -> Vec<String> {
if width == 0 || rows.is_empty() {
return Vec::new();
}
let all_done = rows.iter().all(SubagentRow::is_done);
let theme = agent_chrome_theme();
let chrome = agent_chrome(&theme);
let mut lines = chrome
.subagent_tracker(task)
.slug(workflow_slug(task))
.rows(rows.clone())
.max_running_rows(4)
.margin(2)
.child_indent(5)
.marker("◯")
.accent_color(ACCENT)
.active_color(TN_PURPLE)
.muted_color(TN_GRAY)
.error_color(TN_RED)
.view(width.min(u16::MAX as usize) as u16)
.lines()
.map(str::to_string)
.collect::<Vec<_>>();
if all_done {
lines.extend(
rows.iter()
.rev()
.take(4)
.collect::<Vec<_>>()
.into_iter()
.rev()
.map(|row| completed_subagent_line(row, width)),
);
}
lines
}
fn completed_subagent_line(row: &SubagentRow, width: usize) -> String {
let mark = if row.is_success() { "✓" } else { "✗" };
let left_color = if row.is_success() { TN_GRAY } else { TN_RED };
let left = format!(
" {mark} {} {}",
row.agent_value(),
row.description_value()
);
let mut right = row.elapsed_value().unwrap_or("0.0s").to_string();
if row.tokens_value() > 0 {
right.push_str(&format!(
" · ↓ {} tokens",
humanize(row.tokens_value() as usize)
));
}
fixed_width_status_row(&left, left_color, &right, width)
}
fn fixed_width_status_row(left: &str, left_color: Color, right: &str, width: usize) -> String {
if width == 0 {
return String::new();
}
let right = truncate_visible(right, width);
let reserved = a3s_tui::style::visible_len(&right).saturating_add(1);
let left_width = width.saturating_sub(reserved);
let left = truncate_visible(left, left_width);
let used = a3s_tui::style::visible_len(&left) + a3s_tui::style::visible_len(&right);
let gap = width.saturating_sub(used);
format!(
"{}{}{}",
Style::new().fg(left_color).render(&left),
" ".repeat(gap),
Style::new().fg(TN_GRAY).render(&right)
)
}
fn truncate_visible(text: &str, width: usize) -> String {
if width == 0 {
return String::new();
}
let mut out = String::new();
let mut used = 0usize;
for ch in text.chars() {
let w = a3s_tui::style::visible_len(&ch.to_string());
if used + w > width {
break;
}
out.push(ch);
used += w;
}
out
}
fn task_queue_lines(
completed: usize,
running: Option<&str>,
queued: Vec<QueuedTask>,
width: usize,
) -> Vec<String> {
if width == 0 || queued.is_empty() {
return Vec::new();
}
let theme = agent_chrome_theme();
let chrome = agent_chrome(&theme);
let mut queue = chrome
.task_queue()
.completed(completed)
.queued_tasks(queued)
.margin(2)
.header_color(TN_GRAY)
.running_color(TN_YELLOW)
.queued_color(TN_GRAY);
if let Some(running) = running {
queue = queue.running(running);
}
queue
.view(width.min(u16::MAX as usize) as u16)
.lines()
.map(str::to_string)
.collect()
}
fn plan_checklist_lines(plan: &[(String, String, char, Color)], width: usize) -> Vec<String> {
if width == 0 || plan.is_empty() {
return Vec::new();
}
let items = plan
.iter()
.take(8)
.map(|(_, text, glyph, color)| plan_checklist_item(text, *glyph, *color))
.collect::<Vec<_>>();
let theme = agent_chrome_theme();
let chrome = agent_chrome(&theme);
chrome
.checklist(items)
.indent(2)
.connector(true)
.active_color(TN_ORANGE)
.done_color(TN_GRAY)
.strikethrough_done(false)
.view(width.min(u16::MAX as usize) as u16, 8)
.lines()
.map(str::to_string)
.collect()
}
fn plan_checklist_item(text: &str, glyph: char, color: Color) -> ChecklistItem {
let text_color = if glyph == '✗' { TN_RED } else { TN_GRAY };
match glyph {
'✔' => ChecklistItem::new(text)
.status(ChecklistStatus::Done)
.glyph_color(color)
.text_color(text_color),
'▶' => ChecklistItem::new(text)
.status(ChecklistStatus::Active)
.glyph_color(TN_ORANGE)
.text_color(text_color),
'✗' => ChecklistItem::new(text)
.status(ChecklistStatus::Error)
.glyph_color(color)
.text_color(text_color),
_ => ChecklistItem::new(text)
.status(ChecklistStatus::Pending)
.glyph_color(color)
.text_color(text_color),
}
}
fn workflow_slug(text: &str) -> String {
let mut slug = String::new();
let mut last_dash = false;
for ch in text.chars().flat_map(|ch| ch.to_lowercase()) {
if ch.is_ascii_alphanumeric() {
slug.push(ch);
last_dash = false;
} else if !last_dash && !slug.is_empty() {
slug.push('-');
last_dash = true;
}
if slug.len() >= 24 {
break;
}
}
while slug.ends_with('-') {
slug.pop();
}
if slug.is_empty() {
"parallel-agents".to_string()
} else {
slug
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subagent_tracker_lines_use_shared_component_and_fit_width() {
let lines = subagent_tracker_lines(
"Extract reusable terminal components",
vec![
SubagentRow::new("planner", "map panels")
.done(true)
.elapsed("0.8s")
.tokens(900),
SubagentRow::new("coder", "build tracker")
.elapsed("1.4s")
.tokens(1_500),
],
72,
);
let plain = lines
.iter()
.map(|line| a3s_tui::style::strip_ansi(line))
.collect::<Vec<_>>()
.join("\n");
assert_eq!(lines.len(), 2);
assert!(plain.contains("extract-reusable-term"), "{plain}");
assert!(plain.contains("1 running · 1/2 done"), "{plain}");
assert!(plain.contains("↓ 2.4k tokens"), "{plain}");
assert!(plain.contains("coder build tracker"), "{plain}");
assert!(!plain.contains("planner map panels"), "{plain}");
assert!(
lines
.iter()
.all(|line| a3s_tui::style::visible_len(line) <= 72),
"{plain}"
);
}
#[test]
fn task_queue_lines_use_shared_component_and_sort_queue() {
let lines = task_queue_lines(
3,
Some("running a deliberately long job that must fit"),
vec![
QueuedTask::new("later queued job").priority(4).sequence(2),
QueuedTask::new("first queued job").priority(1).sequence(9),
],
34,
);
let plain = lines
.iter()
.map(|line| a3s_tui::style::strip_ansi(line))
.collect::<Vec<_>>();
assert_eq!(lines.len(), 4);
assert!(plain[0].contains("tasks · ✓ 3 done"), "{plain:?}");
assert!(plain[1].contains("⏳ running"), "{plain:?}");
assert!(plain[2].contains("▱ first queued job"), "{plain:?}");
assert!(plain[3].contains("▱ later queued job"), "{plain:?}");
assert!(
lines
.iter()
.all(|line| a3s_tui::style::visible_len(line) <= 34),
"{plain:?}"
);
}
#[test]
fn plan_checklist_lines_use_shared_component_and_fit_width() {
let plan = vec![
(
"1".to_string(),
"collect enough evidence for a fairly long task".to_string(),
'□',
TN_GRAY,
),
("2".to_string(), "implement".to_string(), '▶', TN_YELLOW),
("3".to_string(), "verify".to_string(), '✔', TN_GRAY),
("4".to_string(), "fix failure".to_string(), '✗', TN_RED),
];
let lines = plan_checklist_lines(&plan, 30);
let plain = lines
.iter()
.map(|line| a3s_tui::style::strip_ansi(line))
.collect::<Vec<_>>()
.join("\n");
assert_eq!(lines.len(), 4);
assert!(plain.contains("⎿ ◻ collect"), "{plain}");
assert!(plain.contains("◼ implement"), "{plain}");
assert!(plain.contains("✔ verify"), "{plain}");
assert!(plain.contains("✗ fix failure"), "{plain}");
assert!(
lines
.iter()
.any(|line| line.contains(&format!("\x1b[{}m◼", TN_ORANGE.fg_ansi()))),
"{lines:?}"
);
assert!(
lines
.iter()
.any(|line| line.contains(&format!("\x1b[{}mimplement", TN_GRAY.fg_ansi()))),
"{lines:?}"
);
assert!(
!lines.iter().any(|line| line.contains("\x1b[9;")),
"pinned plan rows should keep completed text readable: {lines:?}"
);
assert!(
lines
.iter()
.all(|line| a3s_tui::style::visible_len(line) <= 30),
"{plain}"
);
}
#[test]
fn subagent_tracker_lines_keep_recent_completed_rows_visible() {
let lines = subagent_tracker_lines(
"DeepResearch runtime comparison",
vec![
SubagentRow::new("planner", "map sources")
.done(true)
.elapsed("0.8s")
.tokens(900),
SubagentRow::new("reviewer", "cross-check claims")
.done(false)
.elapsed("1.4s")
.tokens(1_500),
],
72,
);
let plain = lines
.iter()
.map(|line| a3s_tui::style::strip_ansi(line))
.collect::<Vec<_>>()
.join("\n");
assert_eq!(lines.len(), 3);
assert!(plain.contains("2/2 agents done"), "{plain}");
assert!(plain.contains("✓ planner map sources"), "{plain}");
assert!(plain.contains("✗ reviewer cross-check claims"), "{plain}");
assert!(
lines
.iter()
.all(|line| a3s_tui::style::visible_len(line) == 72),
"{plain}"
);
}
}