use crate::app::Snapshot;
use crate::attention;
use crate::delete::{self, Claim, Confirmation};
use crate::git_tree::AgentState;
use crate::nav::{self, convs::ConvBall, convs::ConvRow, ws_key};
use crate::projects::join::{self, JoinState};
use crate::ui_state::UiState;
use std::collections::HashSet;
use std::path::Path;
use super::dispatch::Deps;
use super::reply::{Reply, WsRow};
use super::{Query, config, help};
pub mod queue;
pub fn answer(query: &Query, deps: &Deps, ui: &UiState, now_unix: i64) -> Result<Reply, String> {
let snap = &deps.snapshot;
Ok(match query {
Query::Workspaces => Reply::Workspaces(ws_rows(snap, ui)),
Query::Conversations { workspace } => {
Reply::Conversations(conversations(snap, ui, workspace, now_unix))
}
Query::Balls => Reply::Balls(snap.join_rows.clone()),
Query::Board => Reply::Board(crate::board::build(snap, ui, now_unix)),
Query::Attention => Reply::Attention(queue::queue(snap, ui, now_unix)),
Query::Help { verb } => Reply::Help(help::rows(verb.as_deref())),
Query::Search { text } => Reply::Search(crate::search::run(snap, text, &|| true)),
Query::WorkDiff { workspace, file } => {
let attempts = crate::workdiff::read(snap, workspace);
let patch = file
.as_ref()
.and_then(|f| crate::workdiff::patch(&attempts, f));
Reply::WorkDiff { attempts, patch }
}
Query::Ops { max } => {
let skip = snap.ops.len().saturating_sub(*max);
Reply::Ops(snap.ops.iter().skip(skip).cloned().collect())
}
Query::ReadConfig { file } => return config::read(deps, file),
Query::Marks { workspace } => return Ok(config::read_marks(deps, workspace)),
Query::Providers { workspace } => config::providers(deps, workspace),
})
}
pub fn conversations(snap: &Snapshot, ui: &UiState, ws: &Path, now_unix: i64) -> Vec<ConvRow> {
visible_conversations(snap, ui, ws, now_unix, &HashSet::new())
}
pub fn visible_conversations(
snap: &Snapshot,
ui: &UiState,
ws: &Path,
now_unix: i64,
expanded: &HashSet<String>,
) -> Vec<ConvRow> {
let Some(tree) = snap.trees.get(ws) else {
return Vec::new();
};
let key = ws_key(ws);
let seen = |k, w: &str, a: &str, o: &str| ui.is_seen(k, w, a, o);
let ball = |id: &str| conv_ball(snap, id);
let checks = crate::monitor::row::of_rows(&snap.ops);
nav::convs::visible_rows(
&tree.agents,
&key,
&seen,
now_unix,
&ball,
&checks,
expanded,
)
}
pub fn conv_ball(snap: &Snapshot, id: &str) -> ConvBall {
match snap.join_rows.iter().find(|r| r.ball_id == id) {
Some(r) => ConvBall {
id: id.to_owned(),
state: Some(r.state),
title: r.title.clone(),
badge: join::badge(r.state, r.claimant.as_deref()),
},
None => ConvBall {
id: id.to_owned(),
state: None,
title: None,
badge: None,
},
}
}
pub fn workspace_stats(snap: &Snapshot, ui: &UiState, ws: &Path) -> (usize, usize, bool) {
let Some(tree) = snap.trees.get(ws) else {
return (0, 0, false);
};
let key = ws_key(ws);
let seen = |k, w: &str, a: &str, o: &str| ui.is_seen(k, w, a, o);
let mut count = 0;
let mut running = false;
for a in &tree.agents {
if attention::attention(a, &key, &seen).any() {
count += 1;
}
running |= matches!(a.state, AgentState::Live | AgentState::InFlight);
}
(count, tree.agents.len(), running)
}
pub fn ws_rows(snap: &Snapshot, ui: &UiState) -> Vec<WsRow> {
snap.workspaces
.iter()
.map(|w| {
let (attention, agents, running) = workspace_stats(snap, ui, &w.path);
WsRow {
workspace: w.clone(),
attention,
agents,
running,
}
})
.collect()
}
pub fn names_in(snap: &Snapshot, ws: &Path) -> Vec<String> {
snap.trees
.get(ws)
.into_iter()
.flat_map(|t| {
t.agents
.iter()
.filter_map(crate::git_tree::Agent::name_fact)
})
.collect()
}
pub fn confirmation_of(snap: &Snapshot, ws: &Path) -> Option<Confirmation> {
let name = named_leaf(snap, ws)?;
let agents = snap.trees.get(ws).map_or(&[][..], |t| t.agents.as_slice());
Some(delete::confirmation(
&name,
ws,
&nav::convs::liveness(agents),
bound_claims(snap, ws),
))
}
pub fn agent_confirmation_of(
snap: &Snapshot,
ws: &Path,
root: &str,
) -> Option<delete::agent::AgentConfirmation> {
named_leaf(snap, ws)?;
let agents = snap.trees.get(ws).map_or(&[][..], |t| t.agents.as_slice());
Some(delete::agent::confirmation(root, agents))
}
fn named_leaf(snap: &Snapshot, ws: &Path) -> Option<String> {
crate::binding::named_of(&snap.workspaces, ws)
}
fn bound_claims(snap: &Snapshot, ws: &Path) -> Vec<Claim> {
snap.join_rows
.iter()
.filter(|r| r.workspace.as_deref() == Some(ws) && r.state == JoinState::Bound)
.map(|r| Claim {
project: r.project.clone(),
id: r.ball_id.clone(),
})
.collect()
}
#[cfg(test)]
mod tests;