use std::borrow::Cow;
use std::time::Instant;
use anyhow::Result;
use crate::engine::message::{ConversationMessage, MessageKind, MessageView};
use crate::engine::model::Embedder;
use crate::engine::text;
use super::types::{
CompactError, CompactProfiler, SECTION_CONSTRAINTS, SECTION_CRITICAL_CONTEXT, SECTION_DONE,
SECTION_GOAL, SECTION_IN_PROGRESS, SECTION_PROGRESS, TAG_MODIFIED_FILES, TAG_READ_FILES,
};
pub(super) fn compact_summary_text(message: &ConversationMessage) -> Option<Cow<'_, str>> {
let text = match &message.kind {
MessageKind::PiCompaction { summary, .. }
| MessageKind::PiBranchSummary { summary, .. } => Cow::Borrowed(summary.as_str()),
_ => match message.view() {
MessageView::Text { text, .. } | MessageView::Assistant { text, .. } => {
Cow::Owned(text)
}
MessageView::ToolResult(_) | MessageView::Bash(_) => return None,
},
};
is_compact_summary(&text).then_some(text)
}
pub(super) fn is_compaction_record(message: &ConversationMessage) -> bool {
matches!(
&message.kind,
MessageKind::PiCompaction { .. } | MessageKind::PiBranchSummary { .. }
)
}
pub(super) fn is_compact_summary(text: &str) -> bool {
text.lines()
.any(|line| matches_compact_section(line.trim()))
}
pub(super) fn matches_compact_section(line: &str) -> bool {
matches!(
line,
SECTION_GOAL
| SECTION_CONSTRAINTS
| SECTION_PROGRESS
| SECTION_DONE
| SECTION_IN_PROGRESS
| SECTION_CRITICAL_CONTEXT
| TAG_READ_FILES
| TAG_MODIFIED_FILES
)
}
pub(super) fn strip_entry_ref(line: &str) -> &str {
match line.rfind(" (#") {
Some(index) if line.ends_with(')') => &line[..index],
_ => line,
}
}
pub(super) fn entry_ref(entry_id: &str) -> String {
format!("#{entry_id}")
}
pub(super) fn non_empty_lines(value: &str) -> Vec<String> {
text::sanitize(value)
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.filter(|line| !line.starts_with("<skill") && !line.starts_with("</skill"))
.map(ToString::to_string)
.collect()
}
pub(super) fn clean_sentence(value: &str, max_chars: usize) -> String {
let flat = value.split_whitespace().collect::<Vec<_>>().join(" ");
text::clip(flat.trim_matches(['-', '*', ' ']), max_chars)
}
pub(super) fn referenced_sentence(value: &str, max_chars: usize, entry_id: &str) -> String {
format!(
"{} ({})",
clean_sentence(value, max_chars),
entry_ref(entry_id)
)
}
pub(super) fn contains_any(value: &str, needles: &[&str]) -> bool {
needles.iter().any(|needle| value.contains(needle))
}
pub(super) fn push_unique(items: &mut Vec<String>, item: String) {
if !item.is_empty() && !items.iter().any(|existing| existing == &item) {
items.push(item);
}
}
pub(super) fn extend_unique(items: &mut Vec<String>, incoming: impl IntoIterator<Item = String>) {
for item in incoming {
push_unique(items, item);
}
}
pub(super) fn load_embedder<'a>(
embedder: &'a mut Option<Embedder>,
profiler: &mut CompactProfiler,
) -> Result<&'a Embedder> {
if embedder.is_none() {
let started = Instant::now();
*embedder = Some(Embedder::load()?);
profiler.record("bge load", started);
}
embedder
.as_ref()
.ok_or_else(|| CompactError::EmbedderUnavailable.into())
}
pub(super) fn directive_references<'a>(
goals: &'a [String],
outstanding: &'a [String],
) -> Vec<&'a str> {
goals
.iter()
.chain(outstanding)
.map(|item| strip_entry_ref(item))
.filter(|item| !item.trim().is_empty())
.collect()
}
pub(super) fn extract_recent(items: &mut Vec<String>, limit: usize, keep_first: bool) {
if items.len() <= limit {
return;
}
if keep_first {
let tail_start = items.len() - (limit - 1);
let mut capped = Vec::with_capacity(limit);
capped.push(items[0].clone());
capped.extend_from_slice(&items[tail_start..]);
*items = capped;
} else {
let excess = items.len() - limit;
items.drain(0..excess);
}
}