use super::{AppModel, Focus};
use crate::attention;
use crate::git_tree::AgentState;
use crate::keymap::InspectorTab;
use crate::nav::{self, ws_key};
use crate::ui_state::SeenKind;
use std::path::{Path, PathBuf};
enum Pick {
NextAttention,
Step(isize),
}
impl AppModel {
pub fn workspace_stats(&self, ws: &Path) -> (usize, usize, bool) {
let Some(tree) = self.trees.get(ws) else {
return (0, 0, false);
};
let key = ws_key(ws);
let seen = |k, w: &str, a: &str, o: &str| self.ui.is_seen(k, w, a, o);
let mut attention = 0;
let mut running = false;
for a in &tree.agents {
if attention::attention(a, &key, &seen).any() {
attention += 1;
}
running |= matches!(a.state, AgentState::Live | AgentState::InFlight);
}
(attention, tree.agents.len(), running)
}
pub fn strip_total(&self) -> usize {
self.workspaces
.iter()
.map(|w| self.workspace_stats(&w.path).0)
.sum()
}
pub fn tab_bar(&self) -> nav::tabs::TabBar {
let items: Vec<nav::tabs::Item> = self
.workspaces
.iter()
.map(|w| nav::tabs::Item {
ws: w.clone(),
attention: self.workspace_stats(&w.path).0,
})
.collect();
nav::tabs::build(&items, &self.ui.pinned(), self.focus.ws.as_deref())
}
pub fn conversations(&self, now_unix: i64) -> Vec<nav::convs::ConvRow> {
let Some(ws) = self.focus.ws.as_deref() else {
return Vec::new();
};
let Some(tree) = self.trees.get(ws) else {
return Vec::new();
};
let key = ws_key(ws);
let seen = |k, w: &str, a: &str, o: &str| self.ui.is_seen(k, w, a, o);
let ball = |id: &str| self.resolve_conv_ball(id);
nav::convs::build(&tree.agents, &key, &seen, now_unix, &ball)
}
pub fn conversation_members(&self) -> Vec<crate::git_tree::DescentRow> {
let Some(agent) = self.focus.agent.as_deref() else {
return Vec::new();
};
let Some(tree) = self.focused_tree() else {
return Vec::new();
};
match nav::convs::root_of(&tree.agents, agent) {
Some(root) => nav::convs::members(&tree.agents, &root),
None => Vec::new(),
}
}
pub fn activity(&self) -> crate::opslog::Activity {
crate::opslog::activity(&self.ops)
}
pub fn is_collapsed(&self, key: &str) -> bool {
self.ui.is_collapsed(key)
}
pub fn focus_workspace(&mut self, ws: &Path) {
self.focus = Focus {
ws: Some(ws.to_path_buf()),
agent: None,
tab: self.focus.tab,
};
}
pub fn focus_agent(&mut self, ws: &Path, agent_id: &str) {
self.focus = Focus {
ws: Some(ws.to_path_buf()),
agent: Some(agent_id.to_string()),
tab: self.focus.tab,
};
self.record_seen(ws, agent_id);
}
pub fn inspector_tab(&self) -> InspectorTab {
self.focus.tab
}
pub fn select_tab(&mut self, tab: InspectorTab) {
self.focus.tab = tab;
}
pub fn roster_step(&mut self, delta: isize) {
if let Some((ws, agent)) = self.pick_roster(Pick::Step(delta)) {
self.focus_agent(&ws, &agent);
}
}
fn agent_in(&self, ws: &Path, id: &str) -> Option<&crate::git_tree::Agent> {
self.trees.get(ws)?.agents.iter().find(|a| a.agent_id == id)
}
fn record_seen(&mut self, ws: &Path, agent_id: &str) {
let key = ws_key(ws);
for (kind, oid) in self.evidence_oids(ws, agent_id) {
self.ui.record_seen(kind, &key, agent_id, &oid);
}
}
fn evidence_oids(&self, ws: &Path, agent_id: &str) -> Vec<(SeenKind, String)> {
let mut out = Vec::new();
if let Some(agent) = self.agent_in(ws, agent_id) {
let stopped = (agent.state == AgentState::Stopped && agent.abandoned_oid.is_none())
.then(|| agent.tip_oid.clone());
let evidence = [
(SeenKind::Notify, agent.notify_oid.clone()),
(SeenKind::Stopped, stopped),
(SeenKind::Budget, agent.budget_oid.clone()),
(SeenKind::Conflicted, agent.conflicted_oid.clone()),
];
for (kind, oid) in evidence {
if let Some(oid) = oid {
out.push((kind, oid));
}
}
}
out
}
pub fn jump_next_attention(&mut self) {
if let Some((ws, agent)) = self.pick_roster(Pick::NextAttention) {
self.focus_agent(&ws, &agent);
}
}
fn pick_roster(&self, pick: Pick) -> Option<(PathBuf, String)> {
let mut keyed: Vec<(PathBuf, String)> = Vec::new();
for w in &self.workspaces {
if self.trees.contains_key(&w.path) {
keyed.push((w.path.clone(), ws_key(&w.path)));
}
}
keyed.sort_by(|a, b| a.1.cmp(&b.1));
let wss: Vec<(&str, &[crate::git_tree::Agent])> = keyed
.iter()
.filter_map(|(p, k)| self.trees.get(p).map(|t| (k.as_str(), t.agents.as_slice())))
.collect();
let seen = |k, w: &str, a: &str, o: &str| self.ui.is_seen(k, w, a, o);
let roster = attention::roster_order(&wss, &seen);
let key = match pick {
Pick::NextAttention => attention::next_attention(&roster, self.focus_pos()),
Pick::Step(delta) => attention::step(&roster, self.focus_pos(), delta),
};
key.map(|k| (PathBuf::from(k.ws), k.agent_id))
}
fn focus_pos(&self) -> Option<(&str, &str)> {
let ws = self.focus.ws.as_deref()?.to_str()?;
let agent = self.focus.agent.as_deref()?;
Some((ws, agent))
}
pub fn toggle_pin(&mut self, key: &str) {
let mut pins = self.ui.pinned();
match pins.iter().position(|k| k == key) {
Some(i) => {
pins.remove(i);
}
None => pins.push(key.to_string()),
}
self.ui.set_pinned(pins);
}
pub fn set_collapsed(&mut self, key: &str, collapsed: bool) {
self.ui.set_collapsed(key, collapsed);
}
pub fn flush_ui(&mut self) -> std::io::Result<bool> {
self.ui.flush()
}
pub fn is_seen(&self, kind: SeenKind, ws: &str, agent: &str, oid: &str) -> bool {
self.ui.is_seen(kind, ws, agent, oid)
}
}