use super::flight::flight;
use crate::attention;
use crate::git_tree::{Agent, AgentState, DescentRow, children_of};
use crate::nav::convs::Tone;
use crate::ui_state::SeenKind;
pub mod model;
pub use model::{ConvBall, ConvRow};
const PREVIEW_CAP: usize = 80;
pub fn build(
agents: &[Agent],
ws: &str,
seen: &dyn Fn(SeenKind, &str, &str, &str) -> bool,
now_unix: i64,
ball: &dyn Fn(&str) -> ConvBall,
checks: &[crate::monitor::Check],
) -> Vec<ConvRow> {
super::expand::visible(
&super::expand::forest_rows(agents, ws, seen, now_unix, ball, checks),
&std::collections::HashSet::new(),
)
}
pub fn age_label(age_secs: i64) -> String {
let s = age_secs.max(0);
match s {
0..=59 => format!("{s}s"),
60..=3599 => format!("{}m", s / 60),
3600..=86399 => format!("{}h", s / 3600),
_ => format!("{}d", s / 86400),
}
}
pub(super) fn row(
agents: &[Agent],
subtree: &[DescentRow],
ws: &str,
seen: &dyn Fn(SeenKind, &str, &str, &str) -> bool,
now_unix: i64,
ball: &dyn Fn(&str) -> ConvBall,
checks: &[crate::monitor::Check],
) -> (i64, ConvRow) {
let members: Vec<&Agent> = subtree.iter().filter_map(|r| agents.get(r.index)).collect();
let root = members.first().copied();
let root_id = root.map(|a| a.agent_id.clone()).unwrap_or_default();
let conv_ball = root.and_then(|a| a.goal_ball.as_deref()).map(ball);
let (state, uncertain) = agg_state(&members, root);
let last_active = members
.iter()
.map(|a| a.last_action_unix)
.max()
.unwrap_or(0);
let attention_count = members
.iter()
.filter(|a| attention::attention(a, agents, ws, seen).any())
.count();
let row = ConvRow {
direct: children_of(agents, &root_id).len(),
stoppable: crate::actions::stop_enabled(Some(&root_id), agents),
stop_children: crate::actions::stop_children_offered(&root_id, agents),
depth: subtree.first().map_or(0, |r| r.depth),
root_id,
state,
uncertain,
preview: preview(root),
age_secs: (now_unix - last_active).max(0),
last_active_unix: last_active,
flight: flight(&members),
attention: attention_count,
members: members.len(),
ball: conv_ball,
tone: tone_of(root),
failure: root
.and_then(|a| a.failure.as_deref())
.map(crate::git_tree::clause),
name: root.and_then(crate::git_tree::Agent::name_fact),
name_display_only: root.is_some_and(Agent::name_display_only),
verdict: crate::monitor::row::worst(
checks,
ws,
&members
.iter()
.map(|a| a.agent_id.clone())
.collect::<Vec<_>>(),
),
};
(last_active, row)
}
fn tone_of(root: Option<&Agent>) -> Tone {
match root {
Some(agent) if agent.in_memory() => Tone::Weak,
Some(agent) if agent.failure.is_some() => Tone::Bad,
_ => Tone::Plain,
}
}
fn agg_state(members: &[&Agent], root: Option<&Agent>) -> (AgentState, bool) {
for want in [AgentState::InFlight, AgentState::Live] {
if let Some(a) = members.iter().find(|a| a.state == want) {
return (want, a.state_uncertain);
}
}
root.map_or((AgentState::Stopped, false), |a| {
(a.state, a.state_uncertain)
})
}
pub(super) fn preview(root: Option<&Agent>) -> String {
let text = root
.and_then(|a| a.preview.clone().or_else(|| a.stream.text.clone()))
.unwrap_or_default();
text.lines()
.next()
.unwrap_or("")
.chars()
.take(PREVIEW_CAP)
.collect()
}