use super::cmd::{LogEntry, for_each_ref_agents, walk_branch_steps};
use super::detect::extract_request_preview;
use super::marks::Marks;
use super::probe::{LockProbe, WriterProbe};
use super::state::classify;
use super::streaming::streaming_text_from_disk;
use super::tools::tool_calls_from_disk;
use super::{AGENTS_DIR, Agent, CommitNode, GitTreeError, INBOX_DIR, STEPS_DIR};
use std::path::Path;
pub(super) fn build_node(entry: LogEntry) -> CommitNode {
let LogEntry {
oid,
timestamp,
subject,
} = entry;
let short_oid = oid.get(..8).unwrap_or(&oid).to_string();
CommitNode {
oid,
short_oid,
timestamp_unix: timestamp,
subject,
}
}
pub(super) fn enumerate_agents(
workspace: &Path,
git_dir: &Path,
lock: &dyn LockProbe,
writer: &dyn WriterProbe,
) -> Result<Vec<Agent>, GitTreeError> {
let out = for_each_ref_agents(git_dir)?;
let text = String::from_utf8_lossy(&out);
let marks = Marks::from_repo(git_dir)?;
let mut agents = Vec::new();
for line in text.lines() {
if line.is_empty() {
continue;
}
let mut parts = line.splitn(3, ' ');
let branch_name = parts
.next()
.ok_or_else(|| GitTreeError::LogFormat(line.to_string()))?
.to_string();
let tip_oid = parts
.next()
.ok_or_else(|| GitTreeError::LogFormat(line.to_string()))?
.to_string();
let ts_str = parts
.next()
.ok_or_else(|| GitTreeError::LogFormat(line.to_string()))?;
let tip_ts: i64 = ts_str
.parse()
.map_err(|_| GitTreeError::LogFormat(line.to_string()))?;
let agent_id = branch_name
.strip_prefix("agents/")
.unwrap_or(&branch_name)
.to_string();
let tip_short_oid = tip_oid.get(..8).unwrap_or(&tip_oid).to_string();
let steps = walk_branch_steps(git_dir, &branch_name)?;
let (state, state_uncertain) = classify(workspace, &agent_id, lock, writer);
agents.push(Agent {
preview: preview_from_disk(workspace, &agent_id),
streaming_text: streaming_text_from_disk(workspace, &agent_id),
tool_calls: tool_calls_from_disk(workspace, &agent_id),
state,
state_uncertain,
pending_messages: pending_from_disk(workspace, &agent_id),
conflicted_oid: marks.conflicted_oid(&agent_id),
budget_oid: marks.budget_oid(&agent_id),
abandoned_oid: marks.abandoned_oid(&agent_id),
notify_oid: marks.notify_oid(&agent_id),
goal_ball: goal_ball_from_disk(workspace, &agent_id),
branch_name,
agent_id,
tip_oid,
tip_short_oid,
tip_timestamp_unix: tip_ts,
steps,
});
}
Ok(agents)
}
fn goal_ball_from_disk(workspace: &Path, agent_id: &str) -> Option<String> {
let path = workspace.join(AGENTS_DIR).join(agent_id).join("goal.md");
let text = std::fs::read_to_string(&path).ok()?;
crate::start::parse_ball_stamp(&text)
}
fn preview_from_disk(workspace: &Path, agent_id: &str) -> Option<String> {
let path = workspace
.join(STEPS_DIR)
.join(agent_id)
.join("001")
.join("request.json");
let bytes = std::fs::read(&path).ok()?;
extract_request_preview(&bytes)
}
fn pending_from_disk(workspace: &Path, agent_id: &str) -> usize {
let inbox = workspace.join(INBOX_DIR).join(agent_id);
let Ok(entries) = std::fs::read_dir(&inbox) else {
return 0;
};
entries
.flatten()
.filter(|e| e.file_name().to_str().is_some_and(|n| n.ends_with(".md")))
.count()
}