use crate::commands_session::auto_compact_if_needed;
use crate::format::*;
use crate::git::{colorize_diff, run_git};
use crate::prompt::{build_retry_prompt, format_changes, run_prompt, ChangeKind, SessionChanges};
use yoagent::agent::Agent;
use yoagent::*;
pub async fn handle_retry(
agent: &mut Agent,
last_input: &Option<String>,
last_error: &Option<String>,
session_total: &mut Usage,
model: &str,
) -> Option<String> {
match last_input {
Some(prev) => {
let retry_input = build_retry_prompt(prev, last_error);
if last_error.is_some() {
println!("{DIM} (retrying with error context){RESET}");
} else {
println!("{DIM} (retrying last input){RESET}");
}
let outcome = run_prompt(agent, &retry_input, session_total, model).await;
auto_compact_if_needed(agent);
outcome.last_tool_error
}
None => {
eprintln!("{DIM} (nothing to retry — no previous input){RESET}\n");
None
}
}
}
pub fn format_exit_summary(changes: &SessionChanges) -> Option<String> {
let snapshot = changes.snapshot();
if snapshot.is_empty() {
return None;
}
let n = snapshot.len();
let edits = snapshot
.iter()
.filter(|c| c.kind == ChangeKind::Edit)
.count();
let writes = snapshot
.iter()
.filter(|c| c.kind == ChangeKind::Write)
.count();
let mut parts = Vec::new();
if writes > 0 {
parts.push(format!("{writes} written"));
}
if edits > 0 {
parts.push(format!("{edits} edited"));
}
Some(format!(
"Session: {} {} changed ({})",
n,
pluralize(n, "file", "files"),
parts.join(", "),
))
}
fn wants_diff(input: &str) -> bool {
input
.split_whitespace()
.skip(1) .any(|arg| arg == "--diff")
}
fn collect_diffs(paths: &[String]) -> String {
let mut out = String::new();
for path in paths {
let unstaged = run_git(&["diff", "--", path]).unwrap_or_default();
let staged = run_git(&["diff", "--cached", "--", path]).unwrap_or_default();
let combined = match (unstaged.is_empty(), staged.is_empty()) {
(false, false) => format!("{unstaged}\n{staged}"),
(false, true) => unstaged,
(true, false) => staged,
(true, true) => String::new(),
};
if combined.is_empty() {
out.push_str(&format!(" {DIM}({path}: no diff available){RESET}\n"));
} else {
out.push_str(&colorize_diff(&combined));
out.push('\n');
}
}
out
}
pub fn handle_changes(changes: &SessionChanges, input: &str) {
let output = format_changes(changes);
if output.is_empty() {
println!("{DIM} No files modified yet this session.");
println!(
" Files touched by write_file or edit_file tool calls will appear here.{RESET}\n"
);
return;
}
println!("{DIM}{output}{RESET}");
if wants_diff(input) {
let snapshot = changes.snapshot();
let paths: Vec<String> = snapshot.iter().map(|c| c.path.clone()).collect();
let diffs = collect_diffs(&paths);
if !diffs.is_empty() {
println!("{diffs}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_handle_changes_empty_does_not_panic() {
let changes = SessionChanges::new();
handle_changes(&changes, "/changes");
}
#[test]
fn test_handle_changes_with_entries_does_not_panic() {
let changes = SessionChanges::new();
changes.record("src/main.rs", ChangeKind::Write);
changes.record("src/cli.rs", ChangeKind::Edit);
handle_changes(&changes, "/changes");
}
#[test]
fn test_handle_changes_diff_flag_does_not_panic() {
let changes = SessionChanges::new();
handle_changes(&changes, "/changes --diff");
}
#[test]
fn test_handle_changes_diff_flag_with_entries_does_not_panic() {
let changes = SessionChanges::new();
changes.record("src/main.rs", ChangeKind::Write);
handle_changes(&changes, "/changes --diff");
}
#[test]
fn test_wants_diff_flag_parsing() {
assert!(!wants_diff("/changes"));
assert!(wants_diff("/changes --diff"));
assert!(wants_diff("/changes --diff"));
assert!(!wants_diff("/changes --dif"));
assert!(!wants_diff("/changes --verbose"));
}
#[test]
fn test_format_exit_summary_empty_returns_none() {
let changes = SessionChanges::new();
assert_eq!(format_exit_summary(&changes), None);
}
#[test]
fn test_format_exit_summary_single_write() {
let changes = SessionChanges::new();
changes.record("src/main.rs", ChangeKind::Write);
let summary = format_exit_summary(&changes).unwrap();
assert_eq!(summary, "Session: 1 file changed (1 written)");
}
#[test]
fn test_format_exit_summary_single_edit() {
let changes = SessionChanges::new();
changes.record("src/cli.rs", ChangeKind::Edit);
let summary = format_exit_summary(&changes).unwrap();
assert_eq!(summary, "Session: 1 file changed (1 edited)");
}
#[test]
fn test_format_exit_summary_mixed() {
let changes = SessionChanges::new();
changes.record("src/main.rs", ChangeKind::Write);
changes.record("src/cli.rs", ChangeKind::Edit);
changes.record("src/tools.rs", ChangeKind::Edit);
let summary = format_exit_summary(&changes).unwrap();
assert_eq!(summary, "Session: 3 files changed (1 written, 2 edited)");
}
#[test]
fn test_format_exit_summary_all_writes() {
let changes = SessionChanges::new();
changes.record("a.rs", ChangeKind::Write);
changes.record("b.rs", ChangeKind::Write);
let summary = format_exit_summary(&changes).unwrap();
assert_eq!(summary, "Session: 2 files changed (2 written)");
}
#[test]
fn test_changes_command_recognized() {
use crate::commands::{is_unknown_command, KNOWN_COMMANDS};
assert!(!is_unknown_command("/changes"));
assert!(
KNOWN_COMMANDS.contains(&"/changes"),
"/changes should be in KNOWN_COMMANDS"
);
}
#[test]
fn test_changes_command_not_confused_with_other_commands() {
use crate::commands::is_unknown_command;
assert!(is_unknown_command("/changed"));
assert!(!is_unknown_command("/changelog"));
}
}