use std::borrow::Cow;
const DEFAULT_MAX_LENGTH: usize = 60;
const ACTION_VERBS: &[&str] = &[
"reading",
"writing",
"editing",
"searching",
"analyzing",
"creating",
"modifying",
"updating",
"checking",
"running",
"building",
"testing",
"fixing",
"implementing",
"refactoring",
"debugging",
"deploying",
"installing",
"configuring",
"deleting",
"removing",
"moving",
"renaming",
"copying",
"downloading",
"uploading",
"parsing",
"compiling",
"formatting",
"linting",
"reviewing",
"examining",
"looking",
"inspecting",
"exploring",
"scanning",
"fetching",
"loading",
"saving",
"committing",
"pushing",
"pulling",
"merging",
"rebasing",
"cloning",
];
const INTENT_PREFIXES: &[&str] = &[
"I'll ",
"I will ",
"Let me ",
"I need to ",
"I'm going to ",
"I am going to ",
"Now I'll ",
"Now let me ",
"First, I'll ",
"Next, I'll ",
];
pub fn summarize_action(text: &str, max_length: usize) -> String {
let max_len = if max_length == 0 {
DEFAULT_MAX_LENGTH
} else {
max_length
};
if let Some(summary) = extract_from_intent(text) {
return truncate_to(&summary, max_len);
}
if let Some(summary) = extract_action_verb_sentence(text) {
return truncate_to(&summary, max_len);
}
let first = first_sentence(text);
truncate_to(&first, max_len)
}
fn extract_from_intent(text: &str) -> Option<String> {
for prefix in INTENT_PREFIXES {
if let Some(rest) = text.strip_prefix(prefix) {
let sentence = first_clause(rest);
if sentence.is_empty() {
continue;
}
let converted = verb_to_gerund(&sentence);
return Some(capitalize_first(&converted));
}
}
None
}
fn extract_action_verb_sentence(text: &str) -> Option<String> {
let lower = text.to_lowercase();
for verb in ACTION_VERBS {
if let Some(pos) = lower.find(verb) {
if pos > 0 {
let before = text.as_bytes()[pos - 1];
if before != b'\n' && before != b'.' && before != b' ' {
continue;
}
}
let rest = &text[pos..];
let sentence = first_clause(rest);
return Some(capitalize_first(&sentence));
}
}
None
}
fn verb_to_gerund(text: &str) -> String {
let words: Vec<&str> = text.splitn(2, char::is_whitespace).collect();
if words.is_empty() {
return text.to_string();
}
let verb = words[0].to_lowercase();
let rest = if words.len() > 1 { words[1] } else { "" };
if verb.ends_with("ing") {
return text.to_string();
}
let last_char = verb.chars().last();
let second_last = verb.chars().nth(verb.len().saturating_sub(2));
let third_last = verb.chars().nth(verb.len().saturating_sub(3));
let gerund = if verb.ends_with('e') && !verb.ends_with("ee") {
format!("{}ing", &verb[..verb.len() - 1])
} else if verb.len() >= 3
&& last_char.is_some_and(is_consonant)
&& second_last.is_some_and(is_vowel)
&& third_last.is_some_and(is_consonant)
&& !verb.ends_with('w')
&& !verb.ends_with('x')
&& !verb.ends_with('y')
{
format!("{}{}", verb, last_char.unwrap_or_default()) + "ing"
} else {
format!("{verb}ing")
};
if rest.is_empty() {
gerund
} else {
format!("{gerund} {rest}")
}
}
fn is_vowel(c: char) -> bool {
matches!(c, 'a' | 'e' | 'i' | 'o' | 'u')
}
fn is_consonant(c: char) -> bool {
c.is_ascii_alphabetic() && !is_vowel(c)
}
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(c) => c.to_uppercase().to_string() + chars.as_str(),
None => String::new(),
}
}
fn first_sentence(text: &str) -> Cow<'_, str> {
let line = text.lines().next().unwrap_or(text);
if let Some(pos) = line.find(['.', '!', '?']) {
Cow::Borrowed(&line[..pos])
} else {
Cow::Borrowed(line)
}
}
fn first_clause(text: &str) -> String {
let line = text.lines().next().unwrap_or(text);
let end = line.find(['.', ';', '—']).unwrap_or(line.len());
line[..end].trim().to_string()
}
fn truncate_to(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len - 3])
}
}
#[cfg(test)]
#[path = "action_summarizer_tests.rs"]
mod tests;