use super::core::TmaiCore;
use super::types::{AgentDefinitionInfo, AgentSnapshot, ApiError, TeamSummary, TeamTaskInfo};
impl TmaiCore {
pub fn resolve_agent_key(&self, id: &str) -> Result<String, ApiError> {
let state = self.state().read();
Self::resolve_agent_key_in_state(&state, id)
}
pub fn resolve_agent_key_in_state(
state: &crate::state::AppState,
id: &str,
) -> Result<String, ApiError> {
if state.agents.contains_key(id) {
return Ok(id.to_string());
}
if let Some((key, _)) = state.agents.iter().find(|(_, a)| a.stable_id == id) {
return Ok(key.clone());
}
if let Some((key, _)) = state
.agents
.iter()
.find(|(_, a)| a.pty_session_id.as_deref() == Some(id))
{
return Ok(key.clone());
}
Err(ApiError::AgentNotFound {
target: id.to_string(),
})
}
pub fn list_agents(&self) -> Vec<AgentSnapshot> {
let state = self.state().read();
let defs = &state.agent_definitions;
state
.agent_order
.iter()
.filter_map(|id| state.agents.get(id))
.map(|a| {
let mut snap = AgentSnapshot::from_agent(a);
snap.agent_definition = Self::match_agent_definition(a, defs);
snap
})
.collect()
}
pub fn get_agent(&self, id: &str) -> Result<AgentSnapshot, ApiError> {
let state = self.state().read();
let key = Self::resolve_agent_key_in_state(&state, id)?;
let defs = &state.agent_definitions;
let a = state.agents.get(&key).unwrap();
let mut snap = AgentSnapshot::from_agent(a);
snap.agent_definition = Self::match_agent_definition(a, defs);
Ok(snap)
}
pub fn selected_agent(&self) -> Result<AgentSnapshot, ApiError> {
let state = self.state().read();
let defs = &state.agent_definitions;
state
.selected_agent()
.map(|agent| {
let mut snapshot = AgentSnapshot::from_agent(agent);
snapshot.agent_definition = Self::match_agent_definition(agent, defs);
snapshot
})
.ok_or(ApiError::NoSelection)
}
pub fn attention_count(&self) -> usize {
let state = self.state().read();
state.attention_count()
}
pub fn agent_count(&self) -> usize {
let state = self.state().read();
state.agents.len()
}
pub fn agents_needing_attention(&self) -> Vec<AgentSnapshot> {
let state = self.state().read();
state
.agent_order
.iter()
.filter_map(|id| state.agents.get(id))
.filter(|a| a.status.needs_attention())
.map(AgentSnapshot::from_agent)
.collect()
}
pub fn get_preview(&self, id: &str) -> Result<String, ApiError> {
let state = self.state().read();
let key = Self::resolve_agent_key_in_state(&state, id)?;
Ok(state.agents.get(&key).unwrap().last_content_ansi.clone())
}
pub fn get_content(&self, id: &str) -> Result<String, ApiError> {
let state = self.state().read();
let key = Self::resolve_agent_key_in_state(&state, id)?;
Ok(state.agents.get(&key).unwrap().last_content.clone())
}
pub fn get_transcript(
&self,
id: &str,
) -> Result<Vec<crate::transcript::TranscriptRecord>, ApiError> {
let pane_id = {
let state = self.state().read();
let key = Self::resolve_agent_key_in_state(&state, id)?;
let agent = state.agents.get(&key).unwrap();
state
.target_to_pane_id
.get(&agent.id)
.cloned()
.unwrap_or_else(|| agent.id.clone())
};
let registry = match self.transcript_registry() {
Some(reg) => reg,
None => return Ok(Vec::new()),
};
let reg = registry.read();
Ok(reg
.get(&pane_id)
.map(|state| state.recent_records.clone())
.unwrap_or_default())
}
pub fn list_teams(&self) -> Vec<TeamSummary> {
let state = self.state().read();
let mut teams: Vec<TeamSummary> = state
.teams
.values()
.map(TeamSummary::from_snapshot)
.collect();
teams.sort_by(|a, b| a.name.cmp(&b.name));
teams
}
pub fn get_team(&self, name: &str) -> Result<TeamSummary, ApiError> {
let state = self.state().read();
state
.teams
.get(name)
.map(TeamSummary::from_snapshot)
.ok_or_else(|| ApiError::TeamNotFound {
name: name.to_string(),
})
}
pub fn get_team_tasks(&self, name: &str) -> Result<Vec<TeamTaskInfo>, ApiError> {
let state = self.state().read();
state
.teams
.get(name)
.map(|ts| ts.tasks.iter().map(TeamTaskInfo::from_task).collect())
.ok_or_else(|| ApiError::TeamNotFound {
name: name.to_string(),
})
}
pub fn config_audit(&self) -> crate::security::ScanResult {
let dirs: Vec<std::path::PathBuf> = {
let state = self.state().read();
state
.agents
.values()
.map(|a| std::path::PathBuf::from(&a.cwd))
.collect()
};
let result = crate::security::ConfigAuditScanner::scan(&dirs);
{
let mut state = self.state().write();
state.config_audit = Some(result.clone());
}
result
}
pub fn last_config_audit(&self) -> Option<crate::security::ScanResult> {
let state = self.state().read();
state.config_audit.clone()
}
fn match_agent_definition(
agent: &crate::agents::MonitoredAgent,
defs: &[crate::teams::AgentDefinition],
) -> Option<AgentDefinitionInfo> {
if defs.is_empty() {
return None;
}
if let Some(ref team_info) = agent.team_info {
if let Some(ref agent_type) = team_info.agent_type {
if let Some(def) = defs.iter().find(|d| d.name == *agent_type) {
return Some(AgentDefinitionInfo::from_definition(def));
}
}
if let Some(def) = defs.iter().find(|d| d.name == team_info.member_name) {
return Some(AgentDefinitionInfo::from_definition(def));
}
}
None
}
pub fn is_running(&self) -> bool {
let state = self.state().read();
state.running
}
pub fn last_poll(&self) -> Option<chrono::DateTime<chrono::Utc>> {
let state = self.state().read();
state.last_poll
}
pub fn known_directories(&self) -> Vec<String> {
let state = self.state().read();
state.get_known_directories()
}
pub fn list_projects(&self) -> Vec<String> {
let state = self.state().read();
state.registered_projects.clone()
}
pub fn add_project(&self, path: &str) -> Result<(), ApiError> {
let canonical = std::path::Path::new(path);
if !canonical.is_absolute() {
return Err(ApiError::InvalidInput {
message: "Project path must be absolute".to_string(),
});
}
if !canonical.is_dir() {
return Err(ApiError::InvalidInput {
message: format!("Directory does not exist: {}", path),
});
}
let canonical_str = canonical.to_string_lossy().to_string();
let mut state = self.state().write();
if state.registered_projects.contains(&canonical_str) {
return Ok(()); }
state.registered_projects.push(canonical_str);
let projects = state.registered_projects.clone();
drop(state);
crate::config::Settings::save_projects(&projects);
Ok(())
}
pub fn remove_project(&self, path: &str) -> Result<(), ApiError> {
let mut state = self.state().write();
let before = state.registered_projects.len();
state.registered_projects.retain(|p| p != path);
if state.registered_projects.len() == before {
return Err(ApiError::InvalidInput {
message: format!("Project not found: {}", path),
});
}
let projects = state.registered_projects.clone();
drop(state);
crate::config::Settings::save_projects(&projects);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agents::{AgentStatus, AgentType, MonitoredAgent};
use crate::api::builder::TmaiCoreBuilder;
use crate::config::Settings;
use crate::state::AppState;
fn make_core_with_agents(agents: Vec<MonitoredAgent>) -> TmaiCore {
let state = AppState::shared();
{
let mut s = state.write();
s.update_agents(agents);
}
TmaiCoreBuilder::new(Settings::default())
.with_state(state)
.build()
}
fn test_agent(id: &str, status: AgentStatus) -> MonitoredAgent {
let mut agent = MonitoredAgent::new(
id.to_string(),
AgentType::ClaudeCode,
"Title".to_string(),
"/home/user".to_string(),
100,
"main".to_string(),
"win".to_string(),
0,
0,
);
agent.status = status;
agent
}
#[test]
fn test_list_agents_empty() {
let core = TmaiCoreBuilder::new(Settings::default()).build();
assert!(core.list_agents().is_empty());
}
#[test]
fn test_list_agents() {
let core = make_core_with_agents(vec![
test_agent("main:0.0", AgentStatus::Idle),
test_agent(
"main:0.1",
AgentStatus::Processing {
activity: "Bash".to_string(),
},
),
]);
let agents = core.list_agents();
assert_eq!(agents.len(), 2);
}
#[test]
fn test_get_agent_found() {
let core = make_core_with_agents(vec![test_agent("main:0.0", AgentStatus::Idle)]);
let result = core.get_agent("main:0.0");
assert!(result.is_ok());
assert_eq!(result.unwrap().pane_id, "main:0.0");
}
#[test]
fn test_get_agent_not_found() {
let core = TmaiCoreBuilder::new(Settings::default()).build();
let result = core.get_agent("nonexistent");
assert!(matches!(result, Err(ApiError::AgentNotFound { .. })));
}
#[test]
fn test_attention_count() {
let core = make_core_with_agents(vec![
test_agent("main:0.0", AgentStatus::Idle),
test_agent(
"main:0.1",
AgentStatus::AwaitingApproval {
approval_type: crate::agents::ApprovalType::ShellCommand,
details: "rm -rf".to_string(),
},
),
test_agent(
"main:0.2",
AgentStatus::Error {
message: "oops".to_string(),
},
),
]);
assert_eq!(core.attention_count(), 2);
assert_eq!(core.agent_count(), 3);
}
#[test]
fn test_agents_needing_attention() {
let core = make_core_with_agents(vec![
test_agent("main:0.0", AgentStatus::Idle),
test_agent(
"main:0.1",
AgentStatus::AwaitingApproval {
approval_type: crate::agents::ApprovalType::FileEdit,
details: String::new(),
},
),
]);
let attention = core.agents_needing_attention();
assert_eq!(attention.len(), 1);
assert_eq!(attention[0].pane_id, "main:0.1");
}
#[test]
fn test_get_preview() {
let mut agent = test_agent("main:0.0", AgentStatus::Idle);
agent.last_content_ansi = "\x1b[32mHello\x1b[0m".to_string();
agent.last_content = "Hello".to_string();
let core = make_core_with_agents(vec![agent]);
let preview = core.get_preview("main:0.0").unwrap();
assert!(preview.contains("Hello"));
let content = core.get_content("main:0.0").unwrap();
assert_eq!(content, "Hello");
}
#[test]
fn test_list_teams_empty() {
let core = TmaiCoreBuilder::new(Settings::default()).build();
assert!(core.list_teams().is_empty());
}
#[test]
fn test_is_running() {
let core = TmaiCoreBuilder::new(Settings::default()).build();
assert!(core.is_running());
}
#[test]
fn test_get_transcript_no_registry() {
let core = make_core_with_agents(vec![test_agent("main:0.0", AgentStatus::Idle)]);
let records = core.get_transcript("main:0.0").unwrap();
assert!(records.is_empty());
}
#[test]
fn test_get_transcript_agent_not_found() {
let core = TmaiCoreBuilder::new(Settings::default()).build();
let result = core.get_transcript("nonexistent");
assert!(matches!(result, Err(ApiError::AgentNotFound { .. })));
}
#[test]
fn test_get_transcript_with_registry() {
use crate::transcript::types::TranscriptRecord;
use crate::transcript::watcher::new_transcript_registry;
let registry = new_transcript_registry();
{
let mut reg = registry.write();
let mut state = crate::transcript::TranscriptState::new(
"/tmp/test.jsonl".to_string(),
"sess1".to_string(),
"main:0.0".to_string(),
);
state.push_records(vec![
TranscriptRecord::User {
text: "Hello".to_string(),
uuid: None,
timestamp: None,
},
TranscriptRecord::AssistantText {
text: "Hi there".to_string(),
uuid: None,
timestamp: None,
},
]);
reg.insert("main:0.0".to_string(), state);
}
let app_state = AppState::shared();
{
let mut s = app_state.write();
s.update_agents(vec![test_agent("main:0.0", AgentStatus::Idle)]);
}
let core = TmaiCoreBuilder::new(Settings::default())
.with_state(app_state)
.with_transcript_registry(registry)
.build();
let records = core.get_transcript("main:0.0").unwrap();
assert_eq!(records.len(), 2);
}
#[test]
fn test_resolve_agent_key_by_internal_key() {
let core = make_core_with_agents(vec![test_agent("main:0.0", AgentStatus::Idle)]);
assert_eq!(core.resolve_agent_key("main:0.0").unwrap(), "main:0.0");
}
#[test]
fn test_resolve_agent_key_by_stable_id() {
let agent = test_agent("main:0.0", AgentStatus::Idle);
let stable_id = agent.stable_id.clone();
let core = make_core_with_agents(vec![agent]);
assert_eq!(core.resolve_agent_key(&stable_id).unwrap(), "main:0.0");
}
#[test]
fn test_resolve_agent_key_by_pty_session_id() {
let mut agent = test_agent("pty-session-123", AgentStatus::Idle);
agent.pty_session_id = Some("pty-session-123".to_string());
let core = make_core_with_agents(vec![agent]);
assert_eq!(
core.resolve_agent_key("pty-session-123").unwrap(),
"pty-session-123"
);
}
#[test]
fn test_resolve_agent_key_not_found() {
let core = TmaiCoreBuilder::new(Settings::default()).build();
assert!(matches!(
core.resolve_agent_key("nonexistent"),
Err(ApiError::AgentNotFound { .. })
));
}
#[test]
fn test_stable_id_is_unique_per_agent() {
let a1 = test_agent("main:0.0", AgentStatus::Idle);
let a2 = test_agent("main:0.1", AgentStatus::Idle);
assert_ne!(a1.stable_id, a2.stable_id);
assert_eq!(a1.stable_id.len(), 8);
assert_eq!(a2.stable_id.len(), 8);
}
#[test]
fn test_agent_snapshot_returns_stable_id_as_primary() {
let agent = test_agent("main:0.0", AgentStatus::Idle);
let stable_id = agent.stable_id.clone();
let snapshot = AgentSnapshot::from_agent(&agent);
assert_eq!(snapshot.id, stable_id);
assert_eq!(snapshot.pane_id, "main:0.0");
}
}