//! Agent display names: what the agent is working on, taken from the //! terminal title the agent sets (herdr's `terminal_title_stripped`, plus //! pi's title convention). use detect_core::Agent; /// herdr's activity glyphs (src/terminal/title.rs): claude prefixes its title /// with one of these, or a braille spinner frame. const ACTIVITY_GLYPHS: &str = "·✢✳✶✻✽◐◓◑◒"; /// herdr's `stripped_terminal_title`: drop one recognized leading activity /// glyph and the whitespace after it. fn stripped_title(title: &str) -> &str { let title = title.trim(); let mut chars = title.chars(); let Some(first) = chars.next() else { return title; }; let rest = chars.as_str(); let recognized = matches!(first, '\u{2800}'..='\u{28ff}') || ACTIVITY_GLYPHS.contains(first); if recognized && (rest.is_empty() || rest.starts_with(char::is_whitespace)) { rest.trim() } else { title } } /// The agent's session/task name, if its title carries one. /// /// - pi titles itself `π - - `, or /// `π - ` when the session has no name. /// - other agents (claude) use the title as the task, behind an activity /// glyph. /// - tmux's default title is the host name: no name. pub fn display_name(agent: Option, title: &str, cwd: &str, host: &str) -> Option { let title = stripped_title(title); if title.is_empty() || title == host || host.split('.').next() == Some(title) { return None; } let name = match agent { Some(Agent::Pi) => { let rest = title.strip_prefix("π - ")?; let dir = cwd.trim_end_matches('/').rsplit('/').next().unwrap_or(""); if !dir.is_empty() && rest == dir { return None; } rest.strip_suffix(&format!(" - {dir}")).filter(|_| !dir.is_empty()).unwrap_or(rest) } // claude's title before it names a task is just the product name. Some(Agent::Claude) if title == "Claude Code" => return None, _ => title, }; let name = name.trim(); (!name.is_empty()).then(|| name.to_string()) } #[cfg(test)] mod tests { use super::*; const HOST: &str = "Igor-Soarez-H3H1W71DJ0"; #[test] fn pi_session_name_between_prefix_and_directory() { assert_eq!( display_name(Some(Agent::Pi), "π - Ship langgraph 1.2.13 release - x", "/Users/sz/x", HOST), Some("Ship langgraph 1.2.13 release".into()) ); assert_eq!(display_name(Some(Agent::Pi), "π - 9103 - x", "/Users/sz/x", HOST), Some("9103".into())); } #[test] fn pi_without_a_session_name_has_no_name() { assert_eq!(display_name(Some(Agent::Pi), "π - x", "/Users/sz/x", HOST), None); } #[test] fn pi_name_may_contain_the_separator() { assert_eq!( display_name(Some(Agent::Pi), "π - Fix a - b split - dot", "/Users/sz/x/dot", HOST), Some("Fix a - b split".into()) ); } #[test] fn pi_title_after_cd_keeps_the_rest() { assert_eq!( display_name(Some(Agent::Pi), "π - Task - olddir", "/Users/sz/newdir", HOST), Some("Task - olddir".into()) ); } #[test] fn claude_title_loses_its_activity_glyph() { assert_eq!( display_name(Some(Agent::Claude), "✳ Investigate flaky test", "/tmp", HOST), Some("Investigate flaky test".into()) ); assert_eq!( display_name(Some(Agent::Claude), "⠋ Investigate flaky test", "/tmp", HOST), Some("Investigate flaky test".into()) ); } #[test] fn claude_default_title_is_not_a_name() { assert_eq!(display_name(Some(Agent::Claude), "✳ Claude Code", "/tmp", HOST), None); assert_eq!(display_name(Some(Agent::Claude), "Claude Code", "/tmp", HOST), None); } #[test] fn default_tmux_title_is_not_a_name() { assert_eq!(display_name(Some(Agent::Claude), HOST, "/tmp", HOST), None); assert_eq!(display_name(Some(Agent::Claude), "Igor-Soarez-H3H1W71DJ0", "/tmp", "Igor-Soarez-H3H1W71DJ0.local"), None); assert_eq!(display_name(Some(Agent::Claude), "", "/tmp", HOST), None); } }