Skip to main content

codetether_agent/tui/app/
text.rs

1pub fn truncate_preview(text: &str, max_chars: usize) -> String {
2    let mut chars = text.chars();
3    let preview: String = chars.by_ref().take(max_chars).collect();
4    if chars.next().is_some() {
5        format!("{preview}…")
6    } else {
7        preview
8    }
9}
10
11pub fn normalize_slash_command(input: &str) -> String {
12    let trimmed = input.trim();
13    let mut parts = trimmed.splitn(2, char::is_whitespace);
14    let command = parts.next().unwrap_or("");
15    let args = parts.next().unwrap_or("").trim();
16
17    let normalized = match command {
18        "/h" | "/?" => "/help".to_string(),
19        "/s" => "/sessions".to_string(),
20        "/w" => "/swarm".to_string(),
21        "/r" => "/ralph".to_string(),
22        "/b" | "/buslog" | "/logs" => "/bus".to_string(),
23        "/p" => "/protocol".to_string(),
24        "/m" => "/model".to_string(),
25        "/set" => "/settings".to_string(),
26        "/sym" => "/symbols".to_string(),
27        other => other.to_string(),
28    };
29
30    if args.is_empty() {
31        normalized
32    } else {
33        format!("{normalized} {args}")
34    }
35}
36
37/// Easy-mode command aliases that map user-friendly names to advanced commands.
38/// Call this early in the input pipeline before slash command dispatch.
39pub fn normalize_easy_command(input: &str) -> String {
40    let trimmed = input.trim();
41    let mut parts = trimmed.splitn(2, char::is_whitespace);
42    let command = parts.next().unwrap_or("");
43    let args = parts.next().unwrap_or("").trim();
44
45    let normalized = match command {
46        "/add" => {
47            if args.is_empty() {
48                "/spawn".to_string()
49            } else {
50                format!("/spawn {args}")
51            }
52        }
53        "/talk" | "/say" => {
54            if args.is_empty() {
55                "/agent".to_string()
56            } else {
57                format!("/agent {args}")
58            }
59        }
60        "/list" | "/ls" => "/agents".to_string(),
61        "/remove" | "/rm" => {
62            if args.is_empty() {
63                "/kill".to_string()
64            } else {
65                format!("/kill {args}")
66            }
67        }
68        "/focus" => {
69            if args.is_empty() {
70                "/agent".to_string()
71            } else {
72                format!("/agent {}", args.trim_start_matches('@'))
73            }
74        }
75        "/home" | "/main" => "/chat".to_string(),
76        _ => return input.to_string(),
77    };
78
79    normalized
80}
81
82pub fn command_with_optional_args<'a>(input: &'a str, command: &str) -> Option<&'a str> {
83    let trimmed = input.trim();
84    let rest = trimmed.strip_prefix(command)?;
85
86    if rest.is_empty() {
87        return Some("");
88    }
89
90    let first = rest.chars().next()?;
91    if first.is_whitespace() {
92        Some(rest.trim())
93    } else {
94        None
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::{command_with_optional_args, normalize_easy_command, normalize_slash_command};
101
102    #[test]
103    fn normalize_slash_command_preserves_arguments() {
104        assert_eq!(normalize_slash_command("/p events"), "/protocol events");
105    }
106
107    #[test]
108    fn command_with_optional_args_avoids_prefix_collisions() {
109        assert_eq!(command_with_optional_args("/filed", "/file"), None);
110    }
111
112    #[test]
113    fn normalize_easy_command_add_alias() {
114        assert_eq!(normalize_easy_command("/add coder"), "/spawn coder");
115        assert_eq!(normalize_easy_command("/add"), "/spawn");
116    }
117
118    #[test]
119    fn normalize_easy_command_talk_alias() {
120        assert_eq!(
121            normalize_easy_command("/talk coder hello"),
122            "/agent coder hello"
123        );
124        assert_eq!(
125            normalize_easy_command("/say coder hello"),
126            "/agent coder hello"
127        );
128        assert_eq!(normalize_easy_command("/talk"), "/agent");
129    }
130
131    #[test]
132    fn normalize_easy_command_list_alias() {
133        assert_eq!(normalize_easy_command("/list"), "/agents");
134        assert_eq!(normalize_easy_command("/ls"), "/agents");
135    }
136
137    #[test]
138    fn normalize_easy_command_remove_alias() {
139        assert_eq!(normalize_easy_command("/remove coder"), "/kill coder");
140        assert_eq!(normalize_easy_command("/rm coder"), "/kill coder");
141        assert_eq!(normalize_easy_command("/remove"), "/kill");
142    }
143
144    #[test]
145    fn normalize_easy_command_focus_alias() {
146        assert_eq!(normalize_easy_command("/focus coder"), "/agent coder");
147        assert_eq!(normalize_easy_command("/focus @coder"), "/agent coder");
148        assert_eq!(normalize_easy_command("/focus"), "/agent");
149    }
150
151    #[test]
152    fn normalize_easy_command_home_alias() {
153        assert_eq!(normalize_easy_command("/home"), "/chat");
154        assert_eq!(normalize_easy_command("/main"), "/chat");
155    }
156
157    #[test]
158    fn normalize_easy_command_passthrough() {
159        assert_eq!(normalize_easy_command("/help"), "/help");
160        assert_eq!(normalize_easy_command("/spawn coder"), "/spawn coder");
161        assert_eq!(normalize_easy_command("hello world"), "hello world");
162    }
163}