aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/names.rs
blob: ef09988ae3bcd045cfa766e0fbb26b797608d601 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//! 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 `π - <session name> - <cwd basename>`, or
///   `π - <cwd basename>` 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<Agent>, title: &str, cwd: &str, host: &str) -> Option<String> {
    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);
    }
}