use crate::prompt::dispatch::{MESSAGES_DIR, entry};
use crate::prompt::inbox::USER_SENDER;
use brazen::Content;
#[derive(Default)]
pub(super) struct Refs {
pub(super) user_message: Option<String>,
pub(super) errors: Vec<String>,
pub(super) prs: Vec<String>,
pub(super) shas: Vec<String>,
pub(super) paths: Vec<String>,
}
pub(super) fn of(path: &str, content: &str) -> Refs {
let blocks = if path.ends_with(".json") {
entry::blocks(content.as_bytes())
} else {
Vec::new()
};
let mut refs = Refs {
user_message: (is_user_message(path) && !content.trim().is_empty())
.then(|| content.trim().to_string()),
errors: error_lines(&blocks),
..Refs::default()
};
let text = if blocks.is_empty() {
content.to_string()
} else {
blocks.iter().map(block_text).collect::<Vec<_>>().join("\n")
};
for token in text.split(delimiter).map(|t| t.trim_end_matches('.')) {
if let Some(n) = pull_request(token) {
refs.prs.push(format!("#{n}"));
} else if is_sha(token) {
refs.shas.push(token.to_string());
} else if is_path(token) {
refs.paths.push(token.to_string());
}
}
refs
}
fn block_text(block: &Content) -> String {
match block {
Content::Text(t) | Content::Thinking { text: t, .. } => t.clone(),
Content::ToolUse { input, .. } => input.to_string(),
Content::ToolResult { content, .. } => content
.iter()
.map(block_text)
.collect::<Vec<_>>()
.join("\n"),
_ => String::new(),
}
}
fn error_lines(blocks: &[Content]) -> Vec<String> {
blocks
.iter()
.filter(|b| matches!(b, Content::ToolResult { is_error: true, .. }))
.filter_map(|b| {
block_text(b)
.lines()
.rev()
.find(|l| !l.trim().is_empty())
.map(|l| l.trim().to_string())
})
.collect()
}
fn is_user_message(path: &str) -> bool {
path.starts_with(MESSAGES_DIR) && path.ends_with(&format!("-{USER_SENDER}.md"))
}
fn delimiter(c: char) -> bool {
c.is_whitespace() || "\"'`,;()[]{}<>*|=".contains(c)
}
fn pull_request(token: &str) -> Option<&str> {
let rest = match token.strip_prefix('#') {
Some(rest) => rest,
None => token.split("pull/").nth(1)?,
};
let digits = rest.split(|c: char| !c.is_ascii_digit()).next()?;
(!digits.is_empty()).then_some(digits)
}
fn is_sha(token: &str) -> bool {
(7..=40).contains(&token.len())
&& token
.chars()
.all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
&& token.chars().any(|c| c.is_ascii_digit())
&& token.chars().any(|c| c.is_ascii_alphabetic())
}
fn is_path(token: &str) -> bool {
if !token.contains('/') || token.contains("://") {
return false;
}
token
.rsplit('/')
.next()
.and_then(|last| last.rsplit_once('.'))
.is_some_and(|(stem, ext)| {
!stem.is_empty() && !ext.is_empty() && ext.chars().all(|c| c.is_ascii_alphanumeric())
})
}