use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::core::agent::{Delegation, DelegationStatus};
use crate::core::session::SessionId;
use crate::daemon::state::DaemonState;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DelegationRecordView {
pub delegation_id: String,
pub agent: String,
#[serde(default)]
pub agent_id: Option<String>,
#[serde(default)]
pub owner: String,
pub status: DelegationStatus,
pub age_secs: i64,
#[serde(default)]
pub cwd: Option<PathBuf>,
#[serde(default)]
pub worktree_path: Option<PathBuf>,
#[serde(default)]
pub blocks_dispatch: bool,
pub repair_command: String,
}
impl DelegationRecordView {
pub fn of(
state: &DaemonState,
d: &Delegation,
now: chrono::DateTime<chrono::Utc>,
blocks_dispatch: bool,
) -> Self {
Self {
delegation_id: d.id.0.to_string(),
agent: d.agent.clone(),
agent_id: d.agent_id.clone(),
owner: owner_label(state, d.session),
status: d.status,
age_secs: record_age_secs(d, now),
cwd: d.cwd.clone(),
worktree_path: d.worktree_path.clone(),
blocks_dispatch,
repair_command: repair_command(d),
}
}
}
pub fn owner_label(state: &DaemonState, session: SessionId) -> String {
let hyphenated = session.0.hyphenated().to_string();
let bare = session.0.simple().to_string();
let safe = |name: &str| {
let lower = name.trim().to_ascii_lowercase();
!lower.is_empty()
&& uuid::Uuid::parse_str(&lower).is_err()
&& !lower.contains(&hyphenated)
&& !lower.contains(&bare)
};
match state.session(session).map(|s| s.tmux_name) {
Some(name) if safe(&name) => format!("session `{}`", name.trim()),
Some(_) => "a session with no caller-safe name".to_string(),
None => "a session the daemon holds no record of".to_string(),
}
}
pub(crate) fn record_age_secs(d: &Delegation, now: chrono::DateTime<chrono::Utc>) -> i64 {
(now - d.started_at.unwrap_or(d.created_at)).num_seconds()
}
pub fn repair_command(d: &Delegation) -> String {
match d.agent_id.as_deref() {
Some(agent_id) => format!("tm repair delegation {agent_id}"),
None => format!("tm repair delegation --delegation-id {}", d.id.0),
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DelegationListing {
pub cwd: PathBuf,
pub records: Vec<DelegationRecordView>,
}
pub fn list_for_dir(state: &DaemonState, dir: &Path) -> Vec<DelegationRecordView> {
let now = chrono::Utc::now();
let blocking: Vec<_> = state
.shared_tree_records(dir, None, true, None)
.into_iter()
.map(|d| d.id)
.collect();
let mut records: Vec<_> = state
.all_delegations()
.into_iter()
.filter(|d| {
blocking.contains(&d.id)
|| (!d.status.is_terminal()
&& (d.cwd.as_deref() == Some(dir) || d.worktree_path.as_deref() == Some(dir)))
})
.collect();
records.sort_by_key(|d| d.started_at.unwrap_or(d.created_at));
records
.iter()
.map(|d| DelegationRecordView::of(state, d, now, blocking.contains(&d.id)))
.collect()
}
#[cfg(test)]
#[path = "delegation_records_tests.rs"]
pub(crate) mod delegation_records_tests;