// Context gathering for idle prompts. // // Collects: recent git activity, work state, IRC messages. // Notifications are now handled by the notify module and passed // in separately by the caller. use crate::home; use std::fs; use std::process::Command; pub fn recent_commits() -> String { let tools = home().join("bcachefs-tools"); let out = Command::new("git") .args(["-C", &tools.to_string_lossy(), "log", "--oneline", "-5"]) .output() .ok() .and_then(|o| String::from_utf8(o.stdout).ok()) .unwrap_or_default(); let commits: Vec<&str> = out.trim().lines().collect(); if commits.is_empty() { return String::new(); } format!("Recent commits: {}", commits.join(" | ")) } pub fn uncommitted_files() -> String { let tools = home().join("bcachefs-tools"); let out = Command::new("git") .args(["-C", &tools.to_string_lossy(), "diff", "--name-only"]) .output() .ok() .and_then(|o| String::from_utf8(o.stdout).ok()) .unwrap_or_default(); let files: Vec<&str> = out.trim().lines().take(5).collect(); if files.is_empty() { return String::new(); } format!("Uncommitted: {}", files.join(" ")) } pub fn git_context() -> String { let mut parts = Vec::new(); let c = recent_commits(); if !c.is_empty() { parts.push(c); } let u = uncommitted_files(); if !u.is_empty() { parts.push(u); } let ctx = parts.join(" | "); if ctx.len() > 300 { ctx[..300].to_string() } else { ctx } } pub fn work_state() -> String { let path = home().join(".claude/memory/work-state"); match fs::read_to_string(path) { Ok(s) if !s.trim().is_empty() => format!("Current work: {}", s.trim()), _ => String::new(), } } /// Read the last N lines from each per-channel IRC log. pub fn irc_digest() -> String { let ambient = home().join(".claude/memory/irc-ambient"); if !ambient.exists() { return String::new(); } let log_dir = home().join(".claude/irc/logs"); let entries = match fs::read_dir(&log_dir) { Ok(e) => e, Err(_) => return String::new(), }; let mut sections = Vec::new(); for entry in entries.flatten() { let path = entry.path(); let name = match path.file_stem().and_then(|s| s.to_str()) { Some(n) if !n.starts_with("pm-") => n.to_string(), _ => continue, // skip PM logs in digest }; let content = match fs::read_to_string(&path) { Ok(c) if !c.trim().is_empty() => c, _ => continue, }; let lines: Vec<&str> = content.trim().lines().collect(); let tail: Vec<&str> = lines.iter().rev().take(15).rev().copied().collect(); // Strip the unix timestamp prefix for display let display: Vec = tail.iter().map(|l| { if let Some(rest) = l.find(' ').map(|i| &l[i+1..]) { rest.to_string() } else { l.to_string() } }).collect(); sections.push(format!("#{name}:\n{}", display.join("\n"))); } if sections.is_empty() { return String::new(); } sections.sort(); format!("Recent IRC:\n{}", sections.join("\n\n")) } /// Build full context string for a prompt. /// notification_text is passed in from the notify module. pub fn build(include_irc: bool, notification_text: &str) -> String { let mut parts = Vec::new(); let git = git_context(); if !git.is_empty() { parts.push(format!("Context: {git}")); } let ws = work_state(); if !ws.is_empty() { parts.push(ws); } if !notification_text.is_empty() { parts.push(notification_text.to_string()); } if include_irc { let irc = irc_digest(); if !irc.is_empty() { parts.push(irc); } } parts.join("\n") }