changed user choice for TODO stale check

user can ignore, mark stale or quit.
This commit is contained in:
Jochen
2025-11-21 12:35:14 +11:00
parent 84718223bc
commit 551a577ee1
5 changed files with 99 additions and 39 deletions

View File

@@ -98,4 +98,11 @@ impl UiWriter for MachineUiWriter {
println!("PROMPT_USER_YES_NO: {}", message);
true
}
fn prompt_user_choice(&self, message: &str, options: &[&str]) -> usize {
println!("PROMPT_USER_CHOICE: {}", message);
println!("OPTIONS: {:?}", options);
// Default to first option (index 0) for automation
0
}
}

View File

@@ -356,5 +356,27 @@ impl UiWriter for ConsoleUiWriter {
false
}
}
fn prompt_user_choice(&self, message: &str, options: &[&str]) -> usize {
println!("{} ", message);
for (i, option) in options.iter().enumerate() {
println!(" [{}] {}", i + 1, option);
}
print!("Select an option (1-{}): ", options.len());
let _ = io::stdout().flush();
loop {
let mut input = String::new();
if io::stdin().read_line(&mut input).is_ok() {
if let Ok(choice) = input.trim().parse::<usize>() {
if choice > 0 && choice <= options.len() {
return choice - 1;
}
}
}
print!("Invalid choice. Please select (1-{}): ", options.len());
let _ = io::stdout().flush();
}
}
}

View File

@@ -4276,8 +4276,25 @@ impl<W: UiWriter> Agent<W> {
print!("\x07\x07\x07\x07\x07\x07");
let _ = std::io::stdout().flush();
if !self.ui_writer.prompt_user_yes_no("Requirements have changed! Continue?") {
return Ok("❌ User aborted due to stale TODO list.".to_string());
let options = ["Ignore and Continue", "Mark as Stale", "Quit Application"];
let choice = self.ui_writer.prompt_user_choice("Requirements have changed! What would you like to do?", &options);
match choice {
0 => {
// Ignore and Continue
self.ui_writer.print_context_status("⚠️ Ignoring staleness warning.");
}
1 => {
// Mark as Stale
// We return a message to the agent so it knows to regenerate/fix it.
return Ok("⚠️ TODO list is stale (requirements changed). Please regenerate the TODO list to match the new requirements.".to_string());
}
2 => {
// Quit Application
self.ui_writer.print_context_status("❌ Quitting application as requested.");
std::process::exit(0);
}
_ => unreachable!(),
}
}
}

View File

@@ -59,6 +59,10 @@ pub trait UiWriter: Send + Sync {
/// Prompt the user for a yes/no confirmation
fn prompt_user_yes_no(&self, message: &str) -> bool;
/// Prompt the user to choose from a list of options
/// Returns the index of the selected option
fn prompt_user_choice(&self, message: &str, options: &[&str]) -> usize;
}
/// A no-op implementation for when UI output is not needed
@@ -84,4 +88,5 @@ impl UiWriter for NullUiWriter {
fn flush(&self) {}
fn wants_full_output(&self) -> bool { false }
fn prompt_user_yes_no(&self, _message: &str) -> bool { true }
fn prompt_user_choice(&self, _message: &str, _options: &[&str]) -> usize { 0 }
}

View File

@@ -10,6 +10,7 @@ use serial_test::serial;
struct MockUiWriter {
output: Arc<Mutex<Vec<String>>>,
prompt_responses: Arc<Mutex<Vec<bool>>>,
choice_responses: Arc<Mutex<Vec<usize>>>,
}
impl MockUiWriter {
@@ -17,6 +18,7 @@ impl MockUiWriter {
Self {
output: Arc::new(Mutex::new(Vec::new())),
prompt_responses: Arc::new(Mutex::new(Vec::new())),
choice_responses: Arc::new(Mutex::new(Vec::new())),
}
}
@@ -24,6 +26,10 @@ impl MockUiWriter {
self.prompt_responses.lock().unwrap().push(response);
}
fn set_choice_response(&self, response: usize) {
self.choice_responses.lock().unwrap().push(response);
}
fn get_output(&self) -> Vec<String> {
self.output.lock().unwrap().clone()
}
@@ -60,6 +66,10 @@ impl UiWriter for MockUiWriter {
self.output.lock().unwrap().push(format!("PROMPT: {}", message));
self.prompt_responses.lock().unwrap().pop().unwrap_or(true)
}
fn prompt_user_choice(&self, message: &str, options: &[&str]) -> usize {
self.output.lock().unwrap().push(format!("CHOICE: {} Options: {:?}", message, options));
self.choice_responses.lock().unwrap().pop().unwrap_or(0)
}
}
#[tokio::test]
@@ -92,7 +102,7 @@ async fn test_todo_staleness_check_matching_sha() {
#[tokio::test]
#[serial]
async fn test_todo_staleness_check_mismatch_sha_abort() {
async fn test_todo_staleness_check_mismatch_sha_ignore() {
let temp_dir = TempDir::new().unwrap();
let todo_path = temp_dir.path().join("todo.g3.md");
std::env::set_current_dir(&temp_dir).unwrap();
@@ -106,38 +116,7 @@ async fn test_todo_staleness_check_mismatch_sha_abort() {
config.agent.check_todo_staleness = true;
let ui_writer = MockUiWriter::new();
ui_writer.set_prompt_response(false); // Abort
let mut agent = Agent::new_autonomous(config, ui_writer).await.unwrap();
agent.set_requirements_sha(sha_req.to_string());
let tool_call = ToolCall {
tool: "todo_read".to_string(),
args: serde_json::json!({}),
};
let result = agent.execute_tool(&tool_call).await.unwrap();
assert!(result.contains("❌ User aborted due to stale TODO list."));
}
#[tokio::test]
#[serial]
async fn test_todo_staleness_check_mismatch_sha_continue() {
let temp_dir = TempDir::new().unwrap();
let todo_path = temp_dir.path().join("todo.g3.md");
std::env::set_current_dir(&temp_dir).unwrap();
let sha_file = "old_sha";
let sha_req = "new_sha";
let content = format!("{{{{Based on the requirements file with SHA256: {}}}}}\n- [ ] Task 1", sha_file);
std::fs::write(&todo_path, content).unwrap();
let mut config = Config::default();
config.agent.check_todo_staleness = true;
let ui_writer = MockUiWriter::new();
ui_writer.set_prompt_response(true); // Continue
let output_handle = ui_writer.clone(); // Clone to keep handle
ui_writer.set_choice_response(0); // Ignore
let mut agent = Agent::new_autonomous(config, ui_writer).await.unwrap();
agent.set_requirements_sha(sha_req.to_string());
@@ -149,12 +128,42 @@ async fn test_todo_staleness_check_mismatch_sha_continue() {
let result = agent.execute_tool(&tool_call).await.unwrap();
assert!(result.contains("📝 TODO list:"));
let output = output_handle.get_output();
let has_warning = output.iter().any(|s| s.contains("⚠️ TODO list is stale"));
assert!(has_warning, "Should have printed warning to UI");
}
#[tokio::test]
#[serial]
async fn test_todo_staleness_check_mismatch_sha_mark_stale() {
let temp_dir = TempDir::new().unwrap();
let todo_path = temp_dir.path().join("todo.g3.md");
std::env::set_current_dir(&temp_dir).unwrap();
let sha_file = "old_sha";
let sha_req = "new_sha";
let content = format!("{{{{Based on the requirements file with SHA256: {}}}}}\n- [ ] Task 1", sha_file);
std::fs::write(&todo_path, content).unwrap();
let mut config = Config::default();
config.agent.check_todo_staleness = true;
let ui_writer = MockUiWriter::new();
ui_writer.set_choice_response(1); // Mark as Stale
let mut agent = Agent::new_autonomous(config, ui_writer).await.unwrap();
agent.set_requirements_sha(sha_req.to_string());
let tool_call = ToolCall {
tool: "todo_read".to_string(),
args: serde_json::json!({}),
};
let result = agent.execute_tool(&tool_call).await.unwrap();
assert!(result.contains("⚠️ TODO list is stale"));
assert!(result.contains("Please regenerate"));
}
// Note: We cannot easily test "Quit" (index 2) because it calls std::process::exit(0)
// which would kill the test runner. We skip that test case here.
#[tokio::test]
#[serial]
async fn test_todo_staleness_check_disabled() {