use crate::core::cache::SessionCache;
use crate::core::task_relevance::{compute_relevance, parse_task_hints};
use crate::core::tokens::count_tokens;
use crate::tools::CrpMode;
pub fn handle(
cache: &SessionCache,
task: Option<&str>,
path: Option<&str>,
_crp_mode: CrpMode,
) -> String {
let project_root = path
.map(|p| p.to_string())
.unwrap_or_else(|| ".".to_string());
let index =
if let Some(idx) = crate::core::index_orchestrator::try_load_graph_index(&project_root) {
idx
} else {
crate::core::index_orchestrator::ensure_all_background(&project_root);
return format!(
"INDEXING IN PROGRESS\n\n\
The knowledge graph for this project is being built in the background.\n\
Project: {}\n\n\
Because this is a large project, the initial scan may take a moment.\n\
Please try this command again in 1-2 minutes.",
project_root
);
};
let (task_files, task_keywords) = if let Some(task_desc) = task {
parse_task_hints(task_desc)
} else {
(vec![], vec![])
};
let has_task = !task_files.is_empty() || !task_keywords.is_empty();
let mut output = Vec::new();
if has_task {
let relevance = compute_relevance(&index, &task_files, &task_keywords);
if let Some(task_desc) = task {
let file_context: Vec<(String, usize)> = relevance
.iter()
.filter(|r| r.score >= 0.3)
.take(8)
.filter_map(|r| {
std::fs::read_to_string(&r.path)
.ok()
.map(|c| (r.path.clone(), c.lines().count()))
})
.collect();
let briefing = crate::core::task_briefing::build_briefing(task_desc, &file_context);
output.push(crate::core::task_briefing::format_briefing(&briefing));
}
let high: Vec<&_> = relevance.iter().filter(|r| r.score >= 0.8).collect();
let medium: Vec<&_> = relevance
.iter()
.filter(|r| r.score >= 0.3 && r.score < 0.8)
.collect();
let low: Vec<&_> = relevance.iter().filter(|r| r.score < 0.3).collect();
output.push(format!(
"PROJECT OVERVIEW {} files task-filtered",
index.files.len()
));
output.push(String::new());
if !high.is_empty() {
output.push("▸ DIRECTLY RELEVANT (use ctx_read full):".to_string());
for r in &high {
let line_count = file_line_count(&r.path);
let ref_id = cache.get_file_ref_readonly(&r.path);
let ref_str = ref_id.map_or(String::new(), |r| format!("{r}="));
output.push(format!(
" {ref_str}{} {line_count}L score={:.1}",
short_path(&r.path),
r.score
));
}
output.push(String::new());
}
if !medium.is_empty() {
output.push("▸ CONTEXT (use ctx_read signatures/map):".to_string());
for r in medium.iter().take(20) {
let line_count = file_line_count(&r.path);
output.push(format!(
" {} {line_count}L mode={}",
short_path(&r.path),
r.recommended_mode
));
}
if medium.len() > 20 {
output.push(format!(" ... +{} more", medium.len() - 20));
}
output.push(String::new());
}
if !low.is_empty() {
output.push(format!(
"▸ DISTANT ({} files, not loaded unless needed)",
low.len()
));
for r in low.iter().take(10) {
output.push(format!(" {}", short_path(&r.path)));
}
if low.len() > 10 {
output.push(format!(" ... +{} more", low.len() - 10));
}
}
} else {
output.push(format!(
"PROJECT OVERVIEW {} files {} edges",
index.files.len(),
index.edges.len()
));
output.push(String::new());
let mut by_dir: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
for file_entry in index.files.values() {
let dir = std::path::Path::new(&file_entry.path)
.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| ".".to_string());
by_dir
.entry(dir)
.or_default()
.push(short_path(&file_entry.path));
}
for (dir, files) in &by_dir {
let dir_display = truncate_dir_display(dir, 50);
if files.len() <= 5 {
output.push(format!("{dir_display}/ {}", files.join(" ")));
} else {
output.push(format!(
"{dir_display}/ {} +{} more",
files[..3].join(" "),
files.len() - 3
));
}
}
output.push(String::new());
let mut connection_counts: std::collections::HashMap<&str, usize> =
std::collections::HashMap::new();
for edge in &index.edges {
*connection_counts.entry(&edge.from).or_insert(0) += 1;
*connection_counts.entry(&edge.to).or_insert(0) += 1;
}
let mut hubs: Vec<(&&str, &usize)> = connection_counts.iter().collect();
hubs.sort_by(|a, b| b.1.cmp(a.1));
if !hubs.is_empty() {
output.push("HUB FILES (most connected):".to_string());
for (path, count) in hubs.iter().take(8) {
output.push(format!(" {} ({count} edges)", short_path(path)));
}
}
}
let wakeup = build_wakeup_briefing(&project_root, task);
if !wakeup.is_empty() {
output.push(String::new());
output.push(wakeup);
}
let original = count_tokens(&format!("{} files", index.files.len())) * index.files.len();
let compressed = count_tokens(&output.join("\n"));
output.push(String::new());
output.push(crate::core::protocol::format_savings(original, compressed));
output.join("\n")
}
fn build_wakeup_briefing(project_root: &str, task: Option<&str>) -> String {
let mut parts = Vec::new();
if let Some(knowledge) = crate::core::knowledge::ProjectKnowledge::load(project_root) {
let facts_line = knowledge.format_wakeup();
if !facts_line.is_empty() {
parts.push(facts_line);
}
}
if let Some(session) =
crate::core::session::SessionState::load_latest_for_project_root(project_root)
{
if let Some(ref task) = session.task {
parts.push(format!("LAST_TASK:{}", task.description));
}
if !session.decisions.is_empty() {
let recent: Vec<String> = session
.decisions
.iter()
.rev()
.take(3)
.map(|d| d.summary.clone())
.collect();
parts.push(format!("RECENT_DECISIONS:{}", recent.join("|")));
}
}
if let Some(t) = task {
for r in crate::core::prospective_memory::reminders_for_task(project_root, t) {
parts.push(r);
}
}
let registry = crate::core::agents::AgentRegistry::load_or_create();
let active_agents = registry.list_active(Some(project_root));
if !active_agents.is_empty() {
let agents: Vec<String> = active_agents
.iter()
.map(|a| format!("{}({})", a.agent_id, a.role.as_deref().unwrap_or("-")))
.collect();
parts.push(format!("AGENTS:{}", agents.join(",")));
}
if parts.is_empty() {
return String::new();
}
format!("WAKE-UP BRIEFING:\n{}", parts.join("\n"))
}
fn short_path(path: &str) -> String {
let parts: Vec<&str> = path.split('/').collect();
if parts.len() <= 2 {
return path.to_string();
}
parts[parts.len() - 2..].join("/")
}
fn file_line_count(path: &str) -> usize {
std::fs::read_to_string(path)
.map(|c| c.lines().count())
.unwrap_or(0)
}
fn truncate_dir_display(dir: &str, max_len: usize) -> String {
if dir.len() <= max_len {
return dir.to_string();
}
let target = dir.len() - max_len + 3;
let cut = dir[target..]
.char_indices()
.next()
.map(|(i, _)| target + i)
.unwrap_or(dir.len());
format!("...{}", &dir[cut..])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_ascii() {
let dir = "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z";
let result = truncate_dir_display(dir, 20);
assert!(result.starts_with("..."));
assert_eq!(result.len(), 20);
}
#[test]
fn test_truncate_multibyte() {
let dir = "α/β/γ/δ/ε/ζ/η/θ/ι/κ/λ/μ/ν/ξ/ο/π/ρ/σ/τ/υ/φ/χ/ψ/ω";
let result = truncate_dir_display(dir, 20);
assert!(result.starts_with("..."));
let _ = result.as_str();
}
#[test]
fn test_truncate_short() {
let dir = "/short/path";
assert_eq!(truncate_dir_display(dir, 50), dir);
}
}