use anyhow::Result;
use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf;
use super::config::{read_team_config, TeamConfig};
use super::task::{read_all_tasks, TeamTask};
pub fn scan_teams() -> Result<Vec<TeamConfig>> {
let teams_dir = get_teams_dir();
if !teams_dir.exists() {
return Ok(Vec::new());
}
let mut teams = Vec::new();
let entries = std::fs::read_dir(&teams_dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let dir_name = match path.file_name().and_then(|s| s.to_str()) {
Some(name) => name.to_string(),
None => continue,
};
if is_uuid_like(&dir_name) {
continue;
}
let config_path = path.join("config.json");
if !config_path.exists() {
continue;
}
match read_team_config(&config_path, &dir_name) {
Ok(config) => teams.push(config),
Err(e) => {
eprintln!(
"Warning: Failed to read team config for '{}': {}",
dir_name, e
);
}
}
}
teams.sort_by(|a, b| a.team_name.cmp(&b.team_name));
Ok(teams)
}
pub fn scan_tasks(team_name: &str) -> Result<Vec<TeamTask>> {
let tasks_dir = get_tasks_dir().join(team_name);
read_all_tasks(&tasks_dir)
}
pub fn map_members_to_panes(
team: &TeamConfig,
agent_pids: &[(String, u32)], ) -> HashMap<String, String> {
let mut mapping: HashMap<String, String> = HashMap::new();
if !team.members.is_empty() {
heuristic_mapping(team, agent_pids, &mut mapping);
}
mapping
}
fn heuristic_mapping(
team: &TeamConfig,
agent_pids: &[(String, u32)],
mapping: &mut HashMap<String, String>,
) {
let mut session_panes: BTreeMap<String, Vec<&str>> = BTreeMap::new();
for (target, _) in agent_pids {
if let Some(session) = target.split(':').next() {
session_panes
.entry(session.to_string())
.or_default()
.push(target);
}
}
for panes in session_panes.values_mut() {
panes.sort();
}
for panes in session_panes.values() {
if panes.len() == team.members.len() {
for (member, pane_target) in team.members.iter().zip(panes.iter()) {
mapping.insert(member.name.clone(), (*pane_target).to_string());
}
break;
}
}
}
fn get_teams_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join(".claude/teams")
}
fn get_tasks_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join(".claude/tasks")
}
fn is_uuid_like(name: &str) -> bool {
if name.len() != 36 {
return false;
}
let parts: Vec<&str> = name.split('-').collect();
if parts.len() != 5 {
return false;
}
let expected_lengths = [8, 4, 4, 4, 12];
for (part, &expected_len) in parts.iter().zip(expected_lengths.iter()) {
if part.len() != expected_len {
return false;
}
if !part.chars().all(|c| c.is_ascii_hexdigit()) {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_uuid_like() {
assert!(is_uuid_like("550e8400-e29b-41d4-a716-446655440000"));
assert!(is_uuid_like("a1b2c3d4-e5f6-7890-abcd-ef1234567890"));
assert!(!is_uuid_like("my-project"));
assert!(!is_uuid_like("test-team"));
assert!(!is_uuid_like(""));
assert!(!is_uuid_like("not-a-uuid-at-all-nope"));
assert!(!is_uuid_like("550e840-e29b-41d4-a716-446655440000"));
}
#[test]
fn test_heuristic_mapping_count_mismatch() {
let team = TeamConfig {
team_name: "test".to_string(),
description: None,
members: vec![
super::super::config::TeamMember {
name: "lead".to_string(),
agent_id: "id1".to_string(),
agent_type: None,
cwd: None,
},
super::super::config::TeamMember {
name: "dev".to_string(),
agent_id: "id2".to_string(),
agent_type: None,
cwd: None,
},
],
};
let agent_pids = vec![("session:0.0".to_string(), 1234)];
let mut mapping = HashMap::new();
heuristic_mapping(&team, &agent_pids, &mut mapping);
assert!(mapping.is_empty());
}
#[test]
fn test_heuristic_mapping_count_match() {
let team = TeamConfig {
team_name: "test".to_string(),
description: None,
members: vec![
super::super::config::TeamMember {
name: "lead".to_string(),
agent_id: "id1".to_string(),
agent_type: None,
cwd: None,
},
super::super::config::TeamMember {
name: "dev".to_string(),
agent_id: "id2".to_string(),
agent_type: None,
cwd: None,
},
],
};
let agent_pids = vec![
("session:0.0".to_string(), 1234),
("session:0.1".to_string(), 5678),
];
let mut mapping = HashMap::new();
heuristic_mapping(&team, &agent_pids, &mut mapping);
assert_eq!(mapping.len(), 2);
assert_eq!(mapping.get("lead").unwrap(), "session:0.0");
assert_eq!(mapping.get("dev").unwrap(), "session:0.1");
}
}