&str[..n] panics when n falls inside a multi-byte UTF-8 sequence. This crashed the daemon when processing IRC messages containing Hebrew/Yiddish characters (ehashman's messages hit byte 79-81). Replace all byte-index truncation with chars().take(n).collect() in tmux send_prompt preview, notification logging, and git context truncation. Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
140 lines
3.9 KiB
Rust
140 lines
3.9 KiB
Rust
// 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.chars().take(300).collect()
|
|
} 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<String> = 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")
|
|
}
|