Skip to main content

codetether_agent/session/helper/
text.rs

1use crate::provider::{ContentPart, Message, Role};
2use regex::Regex;
3use std::path::Path;
4use std::sync::OnceLock;
5
6pub fn role_label(role: Role) -> &'static str {
7    match role {
8        Role::System => "System",
9        Role::User => "User",
10        Role::Assistant => "Assistant",
11        Role::Tool => "Tool",
12    }
13}
14/// Extracts text content from a list of ContentParts.
15pub fn extract_text_content(parts: &[ContentPart]) -> String {
16    parts
17        .iter()
18        .filter_map(|part| match part {
19            ContentPart::Text { text } => Some(text.as_str()),
20            _ => None,
21        })
22        .collect::<Vec<_>>()
23        .join("\n")
24}
25
26/// Returns the text of the latest user message in the session.
27pub fn latest_user_text(messages: &[Message]) -> Option<String> {
28    messages.iter().rev().find_map(|m| {
29        if m.role != Role::User {
30            return None;
31        }
32        let text = extract_text_content(&m.content);
33        if text.trim().is_empty() {
34            None
35        } else {
36            Some(text)
37        }
38    })
39}
40
41/// Extracts candidate file paths from text that exist in the workspace.
42pub fn extract_candidate_file_paths(text: &str, cwd: &Path, max_files: usize) -> Vec<String> {
43    static FILE_PATH_RE: OnceLock<Regex> = OnceLock::new();
44    let re = FILE_PATH_RE
45        .get_or_init(|| Regex::new(r"(?P<path>[a-zA-Z0-9_\-\./]+\.[a-zA-Z0-9]+)").unwrap());
46
47    let mut out = Vec::new();
48    for cap in re.captures_iter(text) {
49        let Some(path) = cap.name("path").map(|m| m.as_str()) else {
50            continue;
51        };
52        let path_str = path.to_string();
53        if path_str.is_empty() || out.iter().any(|p: &String| p == &path_str) {
54            continue;
55        }
56        if cwd.join(&path_str).exists() {
57            out.push(path_str);
58        }
59        if out.len() >= max_files {
60            break;
61        }
62    }
63    out
64}
65
66/// Truncates a string to max_chars and adds an ellipsis if needed.
67pub fn truncate_with_ellipsis(value: &str, max_chars: usize) -> String {
68    if max_chars == 0 {
69        return String::new();
70    }
71
72    let mut chars = value.chars();
73    let mut output = String::new();
74    for _ in 0..max_chars {
75        if let Some(c) = chars.next() {
76            output.push(c);
77        } else {
78            break;
79        }
80    }
81
82    if chars.next().is_some() {
83        format!("{output}...")
84    } else {
85        output
86    }
87}