use crate::prompt::Error;
use crate::prompt::dispatch::entry;
use crate::template::GitRunner;
use brazen::Content;
use std::path::Path;
const MESSAGES_DIR: &str = "messages";
const TOOL_ORIGIN: &str = "tool";
pub(crate) fn prune_unsettled(worktree: &Path, git: &dyn GitRunner) -> Result<(), Error> {
let entries = sequence(worktree)?;
let Some(cut) = unsettled_from(worktree, &entries)? else {
return Ok(());
};
let mut args: Vec<&str> = vec!["rm", "-q", "--"];
args.extend(entries[cut..].iter().map(String::as_str));
git.run(worktree, &args).map_err(|source| Error::Git {
op: "rm unsettled tool step",
source,
})
}
fn sequence(worktree: &Path) -> Result<Vec<String>, Error> {
let dir = worktree.join(MESSAGES_DIR);
let read = match std::fs::read_dir(&dir) {
Ok(rd) => rd,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(Error::Io(e)),
};
let mut numbered: Vec<(u32, String)> = Vec::new();
for entry in read {
let name = entry.map_err(Error::Io)?.file_name();
let name = name.to_string_lossy().into_owned();
if let Some(seq) = name.split('-').next().and_then(|p| p.parse::<u32>().ok()) {
numbered.push((seq, format!("{MESSAGES_DIR}/{name}")));
}
}
numbered.sort_by_key(|(seq, _)| *seq);
Ok(numbered.into_iter().map(|(_, rel)| rel).collect())
}
fn unsettled_from(worktree: &Path, entries: &[String]) -> Result<Option<usize>, Error> {
let Some(cut) = entries.iter().rposition(|rel| kind(rel) == Kind::Model) else {
return Ok(None);
};
let mut pending: Vec<String> = blocks(worktree, &entries[cut])?
.iter()
.filter_map(|b| match b {
Content::ToolUse { id, .. } => Some(id.clone()),
_ => None,
})
.collect();
let answering = entries[cut + 1..]
.iter()
.filter(|rel| kind(rel) == Kind::Tool);
for rel in answering {
for block in blocks(worktree, rel)? {
if let Content::ToolResult { tool_use_id, .. } = block {
pending.retain(|id| *id != tool_use_id);
}
}
}
Ok((!pending.is_empty()).then_some(cut))
}
fn blocks(worktree: &Path, rel: &str) -> Result<Vec<Content>, Error> {
let bytes = std::fs::read(worktree.join(rel)).map_err(Error::Io)?;
Ok(entry::blocks(&bytes))
}
#[derive(PartialEq, Eq)]
enum Kind {
Message,
Tool,
Model,
}
fn kind(rel: &str) -> Kind {
let path = Path::new(rel);
if path.extension().and_then(|e| e.to_str()) != Some("json") {
return Kind::Message;
}
let stem = path
.file_stem()
.map(|s| s.to_string_lossy())
.unwrap_or_default();
match stem.split_once('-').map(|x| x.1) {
Some(TOOL_ORIGIN) => Kind::Tool,
_ => Kind::Model,
}
}
#[cfg(test)]
mod tests;