use super::Snapshot;
use crate::git_tree::{Agent, AgentState};
use crate::inboxview::{Deposit, InboxEntry};
use std::path::{Path, PathBuf};
use std::sync::Arc;
const SENDER: &str = "user";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Target {
Conversation(String),
Agent(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Echo {
pub(crate) ws: PathBuf,
pub(crate) target: Target,
pub(crate) text: String,
pub(crate) baseline: usize,
pub(crate) at_unix: i64,
}
impl Echo {
pub(crate) fn started(ws: &Path, conversation: &str, goal: &str, at_unix: i64) -> Self {
Self {
ws: ws.to_path_buf(),
target: Target::Conversation(conversation.to_owned()),
text: goal.to_owned(),
baseline: 0,
at_unix,
}
}
pub(crate) fn messaged(
snap: &Snapshot,
ws: &Path,
agent: &str,
content: &str,
at_unix: i64,
) -> Self {
let target = Target::Agent(agent.to_owned());
let baseline = index_of(snap, ws, &target)
.and_then(|i| snap.trees.get(ws)?.agents.get(i))
.map_or(0, |a| a.messages);
Self {
ws: ws.to_path_buf(),
target,
text: content.to_owned(),
baseline,
at_unix,
}
}
pub(crate) fn resolved(&self, derived: &Snapshot) -> Option<String> {
let Target::Conversation(_) = &self.target else {
return None;
};
let i = index_of(derived, &self.ws, &self.target)?;
Some(derived.trees.get(&self.ws)?.agents.get(i)?.agent_id.clone())
}
pub(crate) fn landed(&self, derived: &Snapshot) -> bool {
index_of(derived, &self.ws, &self.target)
.and_then(|i| derived.trees.get(&self.ws)?.agents.get(i))
.is_some_and(|a| a.messages > self.baseline)
}
fn deposit(&self) -> InboxEntry {
InboxEntry {
name: String::new(),
raw: self.text.clone().into_bytes(),
deposit: Deposit {
sender: Some(SENDER.to_owned()),
deposited_at: Some(crate::ui_state::iso8601_extended(self.at_unix)),
body: self.text.clone(),
..Deposit::default()
},
}
}
fn pending_conversation(&self, name: &str) -> Agent {
Agent {
branch_name: format!("agents/{name}"),
agent_id: name.to_owned(),
tip_oid: String::new(),
tip_short_oid: String::new(),
tip_timestamp_unix: self.at_unix,
call_start_unix: None,
last_action_unix: self.at_unix,
messages: 0,
steps: Vec::new(),
preview: Some(self.text.clone()),
stream: crate::git_tree::Stream::default(),
tool_calls: Vec::new(),
state: AgentState::Live,
state_uncertain: false,
pending: vec![self.deposit()],
conflicted_oid: None,
budget_oid: None,
abandoned_oid: None,
notify_oid: None,
held: None,
goal_ball: None,
name: Some(name.to_owned()),
goal_name: None,
}
}
}
fn index_of(snap: &Snapshot, ws: &Path, target: &Target) -> Option<usize> {
snap.trees
.get(ws)?
.agents
.iter()
.position(|a| match target {
Target::Conversation(name) => a.name_fact().as_deref() == Some(name.as_str()),
Target::Agent(id) => &a.agent_id == id,
})
}
pub(crate) fn compose(
derived: &Arc<Snapshot>,
echo: Option<&Echo>,
tail: Option<&super::live::LiveTail>,
) -> Arc<Snapshot> {
if echo.is_none() && tail.is_none() {
return Arc::clone(derived);
}
let mut snap = (**derived).clone();
if let Some(tail) = tail {
super::live::overlay(&mut snap, tail);
}
let Some(echo) = echo else {
return Arc::new(snap);
};
let tree = snap.trees.entry(echo.ws.clone()).or_default();
match index_of(derived, &echo.ws, &echo.target) {
Some(i) => {
if let Some(agent) = tree.agents.get_mut(i) {
agent.pending.push(echo.deposit());
agent.last_action_unix = agent.last_action_unix.max(echo.at_unix);
}
}
None => {
if let Target::Conversation(name) = &echo.target {
tree.agents.push(echo.pending_conversation(name));
}
}
}
Arc::new(snap)
}
#[cfg(test)]
mod tests;