use super::*;
#[derive(Debug, Clone)]
pub(crate) struct RenderedUnit {
pub(crate) body: String,
pub(crate) rendered_chars: usize,
pub(crate) truncated: bool,
pub(crate) elided_chars: usize,
pub(crate) elided_lines: usize,
}
pub(crate) fn render_unit_body(unit: &TurnUnit, cap_override: Option<usize>) -> RenderedUnit {
let cap = cap_override.unwrap_or_else(|| unit.role.cap());
let chars: Vec<char> = unit.text.chars().collect();
let total = chars.len();
if total <= cap {
return RenderedUnit {
body: unit.text.clone(),
rendered_chars: total,
truncated: false,
elided_chars: 0,
elided_lines: 0,
};
}
let head_keep = ((cap as f64) * unit.role.head_frac()).round() as usize;
let head_keep = head_keep.min(cap);
let tail_keep = cap - head_keep;
let head: String = chars[..head_keep].iter().collect();
let tail: String = chars[total - tail_keep..].iter().collect();
let elided_chars = total - cap;
let elided_lines = unit.orig_newlines;
let nl_note = if elided_lines > 0 {
format!(", {elided_lines} lines elided")
} else {
String::new()
};
let body = format!("{head} … [+{elided_chars} chars{nl_note}] … {tail}");
RenderedUnit {
body,
rendered_chars: cap,
truncated: true,
elided_chars,
elided_lines,
}
}
pub(crate) const SUBSTANCE_NOUNS: &[&str] = &[
"passed", "failed", "tests", "test", "errors", "error", "files", "file", "chars", "lines",
"line", "ops", "cases", "case",
];
pub(crate) const FINDING_LEXEMES: &[&str] = &[
"found",
"confirmed",
"verified",
"proven",
"proof",
"root cause",
"root-cause",
"defer",
"deferred",
"fails",
"failed",
"failure",
"error",
"bug",
"correction",
"corrected",
"fix",
"fixed",
"regression",
];
pub(crate) const INTENT_VERB_OPENERS: &[&str] = &[
"let me", "i'll", "i will", "now i", "now let", "next i", "next,", "let's",
];
pub(crate) fn agent_msg_is_rich(text: &str, cfg: &RichnessCfg) -> bool {
if text.chars().count() >= cfg.rich_min_chars {
return true;
}
let lower = text.to_lowercase();
signal_number_of_substance(&lower)
|| signal_commit_hash(&lower)
|| signal_file_line_ref(text)
|| signal_backtick_code(text)
|| signal_finding_lexeme(&lower)
}
pub(crate) fn signal_number_of_substance(lower: &str) -> bool {
let bytes = lower.as_bytes();
let n = bytes.len();
let mut i = 0;
while i < n {
if bytes[i].is_ascii_digit() {
let start = i;
while i < n && bytes[i].is_ascii_digit() {
i += 1;
}
let run_len = i - start;
if ratio_follows(bytes, i) {
return true;
}
if run_len >= 2 {
let mut lo = start.saturating_sub(16);
while lo > 0 && !lower.is_char_boundary(lo) {
lo -= 1;
}
let mut hi = (i + 16).min(n);
while hi < n && !lower.is_char_boundary(hi) {
hi += 1;
}
let window = &lower[lo..hi];
if SUBSTANCE_NOUNS.iter().any(|noun| window.contains(noun)) {
return true;
}
}
continue;
}
i += 1;
}
false
}
pub(crate) fn ratio_follows(bytes: &[u8], mut i: usize) -> bool {
let n = bytes.len();
while i < n && bytes[i] == b' ' {
i += 1;
}
if i < n && bytes[i] == b'/' {
i += 1;
} else if i + 1 < n && &bytes[i..i + 2] == b"of" {
i += 2;
} else {
return false;
}
while i < n && bytes[i] == b' ' {
i += 1;
}
i < n && bytes[i].is_ascii_digit()
}
pub(crate) fn signal_commit_hash(lower: &str) -> bool {
let bytes = lower.as_bytes();
let n = bytes.len();
let is_hex = |b: u8| b.is_ascii_digit() || (b'a'..=b'f').contains(&b);
let mut i = 0;
while i < n {
let prev_alnum = i > 0 && bytes[i - 1].is_ascii_alphanumeric();
if is_hex(bytes[i]) && !prev_alnum {
let start = i;
let mut has_alpha = false;
while i < n && is_hex(bytes[i]) {
if bytes[i].is_ascii_alphabetic() {
has_alpha = true;
}
i += 1;
}
let len = i - start;
let next_alnum = i < n && bytes[i].is_ascii_alphanumeric();
if (7..=40).contains(&len) && has_alpha && !next_alnum {
return true;
}
continue;
}
i += 1;
}
false
}
pub(crate) fn signal_file_line_ref(text: &str) -> bool {
for tok in text.split(|c: char| c.is_whitespace()) {
let tok = tok.trim_matches(|c: char| matches!(c, '`' | '(' | ')' | ',' | ';' | '"'));
if let Some(colon) = tok.rfind(':') {
let (path, after) = tok.split_at(colon);
let line_part = &after[1..];
if !line_part.is_empty()
&& line_part.bytes().all(|b| b.is_ascii_digit())
&& path_has_alpha_extension(path)
{
return true;
}
}
if (tok.starts_with("src/") || tok.starts_with("tests/")) && tok.len() > 4 {
return true;
}
}
false
}
pub(crate) fn path_has_alpha_extension(path: &str) -> bool {
match path.rsplit_once('.') {
Some((stem, ext)) => {
!stem.is_empty() && !ext.is_empty() && ext.chars().all(|c| c.is_ascii_alphabetic())
}
None => false,
}
}
pub(crate) fn signal_backtick_code(text: &str) -> bool {
let first = match text.find('`') {
Some(i) => i,
None => return false,
};
text[first + 1..].contains('`')
}
pub(crate) fn signal_finding_lexeme(lower: &str) -> bool {
FINDING_LEXEMES.iter().any(|lex| lower.contains(lex))
}
pub(crate) fn agent_msg_is_droppable(text: &str, cfg: &RichnessCfg) -> bool {
if agent_msg_is_rich(text, cfg) {
return false;
}
if text.chars().count() >= cfg.declaration_max_chars {
return false;
}
let head: String = text
.trim_start()
.chars()
.take(24)
.collect::<String>()
.to_lowercase();
INTENT_VERB_OPENERS.iter().any(|v| head.starts_with(v))
}