aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/names.rs
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-10-20 13:59:29 +0300
committerIgor Soarez <igor@soarez.org>2026-10-20 13:59:29 +0300
commit6bccc5f786c479a335c55ad83487e077195cff44 (patch)
tree7777eb50a5e264719a92bd34c653aba9edd133c5 /crates/dinggerd/src/names.rs
parent3731a80384f5e2b7d3343eb3e4999f626bbc7f91 (diff)
menu rows lead with the agent's session name; harness and place move below
herdr's sidebar shows the session name in place of the harness label (via the user's herdr-session-name extension). dingger reads it from the title pi already sets ("π - <name> - <dir>"); other agents use herdr's terminal_title_stripped rule, and claude's default "Claude Code" title or tmux's host-name title count as no name. the daemon stores it as @agent_name for every surface. menu rows: colored status symbol + name as the title (the harness, dimmed, only when unnamed), and a native subtitle "session · window · harness", plus how long done/blocked agents have been waiting; one separator between agents waiting for you and the rest. dingger status shows the same.
Diffstat (limited to 'crates/dinggerd/src/names.rs')
-rw-r--r--crates/dinggerd/src/names.rs117
1 files changed, 117 insertions, 0 deletions
diff --git a/crates/dinggerd/src/names.rs b/crates/dinggerd/src/names.rs
new file mode 100644
index 0000000..ef09988
--- /dev/null
+++ b/crates/dinggerd/src/names.rs
@@ -0,0 +1,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);
+ }
+}