feat: Plan Mode interactive flow with approval shortcuts
- Start g3 in plan mode with ' >>' prompt and welcome message - Add is_approval_input() to detect 'approve', 'a', 'yes', etc. and misspellings - Allow trailing punctuation (!, ., ,) on approval words - Call plan_approve tool directly without LLM when approval detected - Add synthetic assistant message after approval for LLM context - Exit plan mode after successful approval, return to 'g3>' prompt - CTRL-D in plan mode exits plan mode first, then exits g3 - /plan command enters plan mode and shows welcome message - Agent mode (--agent) does not start in plan mode - Add CommandResult enum to signal plan mode entry from commands
This commit is contained in:
@@ -16,6 +16,15 @@ use crate::project::load_and_validate_project;
|
|||||||
use crate::template::process_template;
|
use crate::template::process_template;
|
||||||
use crate::task_execution::execute_task_with_retry;
|
use crate::task_execution::execute_task_with_retry;
|
||||||
|
|
||||||
|
/// Result of handling a command.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum CommandResult {
|
||||||
|
/// Command was handled, continue the loop
|
||||||
|
Handled,
|
||||||
|
/// Enter plan mode (after /plan command)
|
||||||
|
EnterPlanMode,
|
||||||
|
}
|
||||||
|
|
||||||
// --- Research command helpers ---
|
// --- Research command helpers ---
|
||||||
|
|
||||||
fn format_research_task_summary(task: &g3_core::pending_research::ResearchTask) -> String {
|
fn format_research_task_summary(task: &g3_core::pending_research::ResearchTask) -> String {
|
||||||
@@ -53,7 +62,7 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
rl: &mut Editor<G3Helper, rustyline::history::DefaultHistory>,
|
rl: &mut Editor<G3Helper, rustyline::history::DefaultHistory>,
|
||||||
show_prompt: bool,
|
show_prompt: bool,
|
||||||
show_code: bool,
|
show_code: bool,
|
||||||
) -> Result<bool> {
|
) -> Result<CommandResult> {
|
||||||
match input {
|
match input {
|
||||||
"/help" => {
|
"/help" => {
|
||||||
output.print("");
|
output.print("");
|
||||||
@@ -78,7 +87,7 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
output.print(" /help - Show this help message");
|
output.print(" /help - Show this help message");
|
||||||
output.print(" exit/quit - Exit the interactive session");
|
output.print(" exit/quit - Exit the interactive session");
|
||||||
output.print("");
|
output.print("");
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
"/compact" => {
|
"/compact" => {
|
||||||
output.print_g3_progress("compacting session");
|
output.print_g3_progress("compacting session");
|
||||||
@@ -93,17 +102,17 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
output.print_g3_status("compacting session", &format!("error: {}", e));
|
output.print_g3_status("compacting session", &format!("error: {}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
"/thinnify" => {
|
"/thinnify" => {
|
||||||
let result = agent.force_thin();
|
let result = agent.force_thin();
|
||||||
G3Status::thin_result(&result);
|
G3Status::thin_result(&result);
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
"/skinnify" => {
|
"/skinnify" => {
|
||||||
let result = agent.force_thin_all();
|
let result = agent.force_thin_all();
|
||||||
G3Status::thin_result(&result);
|
G3Status::thin_result(&result);
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
"/fragments" => {
|
"/fragments" => {
|
||||||
if let Some(session_id) = agent.get_session_id() {
|
if let Some(session_id) = agent.get_session_id() {
|
||||||
@@ -129,7 +138,7 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
} else {
|
} else {
|
||||||
output.print("No active session - fragments are session-scoped.");
|
output.print("No active session - fragments are session-scoped.");
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
cmd if cmd.starts_with("/rehydrate") => {
|
cmd if cmd.starts_with("/rehydrate") => {
|
||||||
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
|
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
|
||||||
@@ -159,7 +168,7 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
output.print("No active session - fragments are session-scoped.");
|
output.print("No active session - fragments are session-scoped.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
cmd if cmd == "/research" || cmd.starts_with("/research ") => {
|
cmd if cmd == "/research" || cmd.starts_with("/research ") => {
|
||||||
let manager = agent.get_pending_research_manager();
|
let manager = agent.get_pending_research_manager();
|
||||||
@@ -209,7 +218,7 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
cmd if cmd.starts_with("/run") => {
|
cmd if cmd.starts_with("/run") => {
|
||||||
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
|
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
|
||||||
@@ -245,7 +254,7 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
"/dump" => {
|
"/dump" => {
|
||||||
// Dump entire context window to a file for debugging
|
// Dump entire context window to a file for debugging
|
||||||
@@ -253,7 +262,7 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
if !dump_dir.exists() {
|
if !dump_dir.exists() {
|
||||||
if let Err(e) = std::fs::create_dir_all(dump_dir) {
|
if let Err(e) = std::fs::create_dir_all(dump_dir) {
|
||||||
output.print(&format!("❌ Failed to create tmp directory: {}", e));
|
output.print(&format!("❌ Failed to create tmp directory: {}", e));
|
||||||
return Ok(true);
|
return Ok(CommandResult::Handled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,7 +303,7 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
}
|
}
|
||||||
Err(e) => output.print(&format!("❌ Failed to write dump: {}", e)),
|
Err(e) => output.print(&format!("❌ Failed to write dump: {}", e)),
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
"/clear" => {
|
"/clear" => {
|
||||||
use crate::g3_status::G3Status;
|
use crate::g3_status::G3Status;
|
||||||
@@ -302,7 +311,7 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
agent.clear_session();
|
agent.clear_session();
|
||||||
G3Status::done();
|
G3Status::done();
|
||||||
output.print("Starting fresh.");
|
output.print("Starting fresh.");
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
"/readme" => {
|
"/readme" => {
|
||||||
use crate::g3_status::G3Status;
|
use crate::g3_status::G3Status;
|
||||||
@@ -319,12 +328,12 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
G3Status::error(&e.to_string());
|
G3Status::error(&e.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
"/stats" => {
|
"/stats" => {
|
||||||
let stats = agent.get_stats();
|
let stats = agent.get_stats();
|
||||||
output.print(&stats);
|
output.print(&stats);
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
"/resume" => {
|
"/resume" => {
|
||||||
output.print("📋 Scanning for available sessions...");
|
output.print("📋 Scanning for available sessions...");
|
||||||
@@ -333,7 +342,7 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
Ok(sessions) => {
|
Ok(sessions) => {
|
||||||
if sessions.is_empty() {
|
if sessions.is_empty() {
|
||||||
output.print("No sessions found for this directory.");
|
output.print("No sessions found for this directory.");
|
||||||
return Ok(true);
|
return Ok(CommandResult::Handled);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current session ID to mark it
|
// Get current session ID to mark it
|
||||||
@@ -408,7 +417,7 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
}
|
}
|
||||||
Err(e) => output.print(&format!("❌ Error listing sessions: {}", e)),
|
Err(e) => output.print(&format!("❌ Error listing sessions: {}", e)),
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
cmd if cmd.starts_with("/project") => {
|
cmd if cmd.starts_with("/project") => {
|
||||||
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
|
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
|
||||||
@@ -451,7 +460,7 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
cmd if cmd.starts_with("/plan") => {
|
cmd if cmd.starts_with("/plan") => {
|
||||||
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
|
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
|
||||||
@@ -463,6 +472,7 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
output.print(" 3. Request approval before coding");
|
output.print(" 3. Request approval before coding");
|
||||||
output.print("");
|
output.print("");
|
||||||
output.print("Example: /plan Add CSV import for comic book metadata");
|
output.print("Example: /plan Add CSV import for comic book metadata");
|
||||||
|
Ok(CommandResult::Handled)
|
||||||
} else {
|
} else {
|
||||||
let feature_description = parts[1].trim();
|
let feature_description = parts[1].trim();
|
||||||
|
|
||||||
@@ -478,9 +488,14 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
feature_description
|
feature_description
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Print the welcome message for plan mode
|
||||||
|
output.print(" what shall we build today?");
|
||||||
|
|
||||||
execute_task_with_retry(agent, &prompt, show_prompt, show_code, output).await;
|
execute_task_with_retry(agent, &prompt, show_prompt, show_code, output).await;
|
||||||
|
|
||||||
|
// Return EnterPlanMode to signal interactive loop to switch prompts
|
||||||
|
Ok(CommandResult::EnterPlanMode)
|
||||||
}
|
}
|
||||||
Ok(true)
|
|
||||||
}
|
}
|
||||||
"/unproject" => {
|
"/unproject" => {
|
||||||
if active_project.is_some() {
|
if active_project.is_some() {
|
||||||
@@ -494,14 +509,14 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
} else {
|
} else {
|
||||||
output.print("No project is currently loaded.");
|
output.print("No project is currently loaded.");
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
output.print(&format!(
|
output.print(&format!(
|
||||||
"❌ Unknown command: {}. Type /help for available commands.",
|
"❌ Unknown command: {}. Type /help for available commands.",
|
||||||
input
|
input
|
||||||
));
|
));
|
||||||
Ok(true)
|
Ok(CommandResult::Handled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ use tokio::sync::broadcast;
|
|||||||
|
|
||||||
use g3_core::ui_writer::UiWriter;
|
use g3_core::ui_writer::UiWriter;
|
||||||
use g3_core::Agent;
|
use g3_core::Agent;
|
||||||
|
use g3_core::ToolCall;
|
||||||
|
|
||||||
use crate::commands::handle_command;
|
use crate::commands::{handle_command, CommandResult};
|
||||||
use crate::display::{LoadedContent, print_loaded_status, print_project_heading, print_workspace_path};
|
use crate::display::{LoadedContent, print_loaded_status, print_project_heading, print_workspace_path};
|
||||||
use crate::g3_status::{G3Status, Status};
|
use crate::g3_status::{G3Status, Status};
|
||||||
use crate::project::Project;
|
use crate::project::Project;
|
||||||
@@ -25,15 +26,21 @@ use crate::template::process_template;
|
|||||||
use crate::task_execution::execute_task_with_retry;
|
use crate::task_execution::execute_task_with_retry;
|
||||||
use crate::utils::display_context_progress;
|
use crate::utils::display_context_progress;
|
||||||
|
|
||||||
|
/// Plan mode prompt string.
|
||||||
|
const PLAN_MODE_PROMPT: &str = " >> ";
|
||||||
|
|
||||||
/// Build the interactive prompt string.
|
/// Build the interactive prompt string.
|
||||||
///
|
///
|
||||||
/// Format:
|
/// Format:
|
||||||
/// - Multiline mode: `"... > "`
|
/// - Multiline mode: `"... > "`
|
||||||
|
/// - Plan mode: `" >> "`
|
||||||
/// - No project: `"agent_name> "` (defaults to "g3")
|
/// - No project: `"agent_name> "` (defaults to "g3")
|
||||||
/// - With project: `"agent_name | project_name> "`
|
/// - With project: `"agent_name | project_name> "`
|
||||||
pub fn build_prompt(in_multiline: bool, agent_name: Option<&str>, active_project: &Option<Project>) -> String {
|
pub fn build_prompt(in_multiline: bool, in_plan_mode: bool, agent_name: Option<&str>, active_project: &Option<Project>) -> String {
|
||||||
if in_multiline {
|
if in_multiline {
|
||||||
"... > ".to_string()
|
"... > ".to_string()
|
||||||
|
} else if in_plan_mode {
|
||||||
|
PLAN_MODE_PROMPT.to_string()
|
||||||
} else {
|
} else {
|
||||||
let base_name = agent_name.unwrap_or("g3");
|
let base_name = agent_name.unwrap_or("g3");
|
||||||
if let Some(project) = active_project {
|
if let Some(project) = active_project {
|
||||||
@@ -48,6 +55,63 @@ pub fn build_prompt(in_multiline: bool, agent_name: Option<&str>, active_project
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if the input is an approval command (for plan mode).
|
||||||
|
///
|
||||||
|
/// Recognizes: "a", "approve", "approved", and common misspellings.
|
||||||
|
pub fn is_approval_input(input: &str) -> bool {
|
||||||
|
let normalized = input.trim().to_lowercase();
|
||||||
|
// Strip trailing punctuation (!, ., ,)
|
||||||
|
let normalized = normalized.trim_end_matches(|c| c == '!' || c == '.' || c == ',');
|
||||||
|
|
||||||
|
// Exact matches
|
||||||
|
if matches!(normalized, "a" | "approve" | "approved" | "yes" | "y" | "ok") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common misspellings of "approve" / "approved"
|
||||||
|
let misspellings = [
|
||||||
|
"approv", // missing 'e'
|
||||||
|
"aprove", // missing 'p'
|
||||||
|
"aproved", // missing 'p'
|
||||||
|
"aprrove", // transposed
|
||||||
|
"appprove", // extra 'p'
|
||||||
|
"apporve", // transposed
|
||||||
|
"approev", // transposed
|
||||||
|
"approvd", // missing 'e'
|
||||||
|
"approed", // missing 'v'
|
||||||
|
"approvee", // extra 'e'
|
||||||
|
"approveed", // extra 'e'
|
||||||
|
];
|
||||||
|
|
||||||
|
misspellings.iter().any(|&m| normalized == m)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute plan_approve tool directly without going through the LLM.
|
||||||
|
///
|
||||||
|
/// Returns (success, message) where success indicates if the plan was approved.
|
||||||
|
async fn execute_plan_approve_directly<W: UiWriter>(
|
||||||
|
agent: &mut Agent<W>,
|
||||||
|
output: &SimpleOutput,
|
||||||
|
) -> (bool, String) {
|
||||||
|
let tool_call = ToolCall {
|
||||||
|
tool: "plan_approve".to_string(),
|
||||||
|
args: serde_json::json!({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
match agent.execute_tool_call(&tool_call).await {
|
||||||
|
Ok(result) => {
|
||||||
|
let success = result.contains("✅ Plan approved");
|
||||||
|
output.print(&result);
|
||||||
|
(success, result)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let msg = format!("❌ Failed to approve plan: {}", e);
|
||||||
|
output.print(&msg);
|
||||||
|
(false, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Execute user input with template processing and auto-memory reminder.
|
/// Execute user input with template processing and auto-memory reminder.
|
||||||
///
|
///
|
||||||
/// This is the common path for both single-line and multiline input.
|
/// This is the common path for both single-line and multiline input.
|
||||||
@@ -183,12 +247,6 @@ pub async fn run_interactive<W: UiWriter>(
|
|||||||
|
|
||||||
// Skip verbose welcome when coming from agent mode (it already printed context info)
|
// Skip verbose welcome when coming from agent mode (it already printed context info)
|
||||||
if !from_agent_mode {
|
if !from_agent_mode {
|
||||||
output.print("");
|
|
||||||
output.print("g3 programming agent");
|
|
||||||
output.print(" >> what shall we build today?");
|
|
||||||
output.print("");
|
|
||||||
|
|
||||||
// Display provider and model information
|
|
||||||
match agent.get_provider_info() {
|
match agent.get_provider_info() {
|
||||||
Ok((provider, model)) => {
|
Ok((provider, model)) => {
|
||||||
print!(
|
print!(
|
||||||
@@ -220,8 +278,15 @@ pub async fn run_interactive<W: UiWriter>(
|
|||||||
|
|
||||||
// Display workspace path
|
// Display workspace path
|
||||||
print_workspace_path(workspace_path);
|
print_workspace_path(workspace_path);
|
||||||
|
|
||||||
|
// Print welcome message right before the prompt
|
||||||
output.print("");
|
output.print("");
|
||||||
|
output.print("g3 programming agent");
|
||||||
|
output.print(" what shall we build today?");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track plan mode state (start in plan mode for non-agent mode)
|
||||||
|
let mut in_plan_mode = !from_agent_mode;
|
||||||
|
|
||||||
// Initialize rustyline editor with history
|
// Initialize rustyline editor with history
|
||||||
let config = Config::builder()
|
let config = Config::builder()
|
||||||
@@ -284,7 +349,7 @@ pub async fn run_interactive<W: UiWriter>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build prompt
|
// Build prompt
|
||||||
let prompt = build_prompt(in_multiline, agent_name, &active_project);
|
let prompt = build_prompt(in_multiline, in_plan_mode, agent_name, &active_project);
|
||||||
|
|
||||||
// Update the shared prompt for the notification handler
|
// Update the shared prompt for the notification handler
|
||||||
*current_prompt.write().unwrap() = prompt.clone();
|
*current_prompt.write().unwrap() = prompt.clone();
|
||||||
@@ -335,6 +400,31 @@ pub async fn run_interactive<W: UiWriter>(
|
|||||||
// Single line input
|
// Single line input
|
||||||
let input = line.trim().to_string();
|
let input = line.trim().to_string();
|
||||||
|
|
||||||
|
// In plan mode, check for approval input before anything else
|
||||||
|
if in_plan_mode && is_approval_input(&input) {
|
||||||
|
// Add to history
|
||||||
|
rl.add_history_entry(&input)?;
|
||||||
|
|
||||||
|
// Reprint input with formatting
|
||||||
|
reprint_formatted_input(&input, &prompt);
|
||||||
|
|
||||||
|
is_busy.store(true, Ordering::SeqCst);
|
||||||
|
let (approved, result) = execute_plan_approve_directly(&mut agent, &output).await;
|
||||||
|
is_busy.store(false, Ordering::SeqCst);
|
||||||
|
|
||||||
|
if approved {
|
||||||
|
// Exit plan mode on successful approval
|
||||||
|
in_plan_mode = false;
|
||||||
|
|
||||||
|
// Add synthetic assistant message so LLM knows plan was approved
|
||||||
|
use g3_providers::{Message, MessageRole};
|
||||||
|
let synthetic_msg = Message::new(MessageRole::Assistant, result);
|
||||||
|
agent.add_message_to_context(synthetic_msg);
|
||||||
|
}
|
||||||
|
// Stay in plan mode if approval failed
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if input.is_empty() {
|
if input.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -349,11 +439,17 @@ pub async fn run_interactive<W: UiWriter>(
|
|||||||
// Check for control commands
|
// Check for control commands
|
||||||
if input.starts_with('/') {
|
if input.starts_with('/') {
|
||||||
is_busy.store(true, Ordering::SeqCst);
|
is_busy.store(true, Ordering::SeqCst);
|
||||||
let handled = handle_command(&input, &mut agent, workspace_path, &output, &mut active_project, &mut rl, show_prompt, show_code).await?;
|
let result = handle_command(&input, &mut agent, workspace_path, &output, &mut active_project, &mut rl, show_prompt, show_code).await?;
|
||||||
is_busy.store(false, Ordering::SeqCst);
|
is_busy.store(false, Ordering::SeqCst);
|
||||||
|
|
||||||
if handled {
|
match result {
|
||||||
continue;
|
CommandResult::Handled => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CommandResult::EnterPlanMode => {
|
||||||
|
in_plan_mode = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,8 +476,16 @@ pub async fn run_interactive<W: UiWriter>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(ReadlineError::Eof) => {
|
Err(ReadlineError::Eof) => {
|
||||||
output.print("CTRL-D");
|
// CTRL-D: if in plan mode, exit plan mode first; otherwise exit g3
|
||||||
break;
|
if in_plan_mode {
|
||||||
|
output.print("CTRL-D (exiting plan mode)");
|
||||||
|
in_plan_mode = false;
|
||||||
|
// Continue the loop with normal prompt
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
output.print("CTRL-D");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error: {:?}", err);
|
error!("Error: {:?}", err);
|
||||||
@@ -425,36 +529,54 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_prompt_default() {
|
fn test_build_prompt_default() {
|
||||||
let prompt = build_prompt(false, None, &None);
|
let prompt = build_prompt(false, false, None, &None);
|
||||||
assert_eq!(prompt, "g3> ");
|
assert_eq!(prompt, "g3> ");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_prompt_with_agent_name() {
|
fn test_build_prompt_with_agent_name() {
|
||||||
let prompt = build_prompt(false, Some("butler"), &None);
|
let prompt = build_prompt(false, false, Some("butler"), &None);
|
||||||
assert_eq!(prompt, "butler> ");
|
assert_eq!(prompt, "butler> ");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_prompt_multiline() {
|
fn test_build_prompt_multiline() {
|
||||||
let prompt = build_prompt(true, None, &None);
|
let prompt = build_prompt(true, false, None, &None);
|
||||||
assert_eq!(prompt, "... > ");
|
assert_eq!(prompt, "... > ");
|
||||||
|
|
||||||
// Multiline takes precedence over agent name
|
// Multiline takes precedence over agent name
|
||||||
let prompt = build_prompt(true, Some("butler"), &None);
|
let prompt = build_prompt(true, false, Some("butler"), &None);
|
||||||
assert_eq!(prompt, "... > ");
|
assert_eq!(prompt, "... > ");
|
||||||
|
|
||||||
// Multiline takes precedence over project
|
// Multiline takes precedence over project
|
||||||
let project = Some(create_test_project("myapp"));
|
let project = Some(create_test_project("myapp"));
|
||||||
let prompt = build_prompt(true, None, &project);
|
let prompt = build_prompt(true, false, None, &project);
|
||||||
assert_eq!(prompt, "... > ");
|
assert_eq!(prompt, "... > ");
|
||||||
|
|
||||||
|
// Multiline takes precedence over plan mode
|
||||||
|
let prompt = build_prompt(true, true, None, &None);
|
||||||
|
assert_eq!(prompt, "... > ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_prompt_plan_mode() {
|
||||||
|
let prompt = build_prompt(false, true, None, &None);
|
||||||
|
assert_eq!(prompt, " >> ");
|
||||||
|
|
||||||
|
// Plan mode takes precedence over agent name
|
||||||
|
let prompt = build_prompt(false, true, Some("butler"), &None);
|
||||||
|
assert_eq!(prompt, " >> ");
|
||||||
|
|
||||||
|
// Plan mode takes precedence over project
|
||||||
|
let project = Some(create_test_project("myapp"));
|
||||||
|
let prompt = build_prompt(false, true, None, &project);
|
||||||
|
assert_eq!(prompt, " >> ");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_prompt_with_project() {
|
fn test_build_prompt_with_project() {
|
||||||
let project = Some(create_test_project("myapp"));
|
let project = Some(create_test_project("myapp"));
|
||||||
let prompt = build_prompt(false, None, &project);
|
let prompt = build_prompt(false, false, None, &project);
|
||||||
// Should contain the project name in the prompt
|
|
||||||
assert!(prompt.contains("g3"));
|
assert!(prompt.contains("g3"));
|
||||||
assert!(prompt.contains("myapp"));
|
assert!(prompt.contains("myapp"));
|
||||||
assert!(prompt.contains("|"));
|
assert!(prompt.contains("|"));
|
||||||
@@ -463,8 +585,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_build_prompt_with_agent_and_project() {
|
fn test_build_prompt_with_agent_and_project() {
|
||||||
let project = Some(create_test_project("myapp"));
|
let project = Some(create_test_project("myapp"));
|
||||||
let prompt = build_prompt(false, Some("carmack"), &project);
|
let prompt = build_prompt(false, false, Some("carmack"), &project);
|
||||||
// Should contain both agent name and project name
|
|
||||||
assert!(prompt.contains("carmack"));
|
assert!(prompt.contains("carmack"));
|
||||||
assert!(prompt.contains("myapp"));
|
assert!(prompt.contains("myapp"));
|
||||||
assert!(prompt.contains("|"));
|
assert!(prompt.contains("|"));
|
||||||
@@ -474,25 +595,60 @@ mod tests {
|
|||||||
fn test_build_prompt_unproject_resets() {
|
fn test_build_prompt_unproject_resets() {
|
||||||
// Simulate /project loading
|
// Simulate /project loading
|
||||||
let project = Some(create_test_project("myapp"));
|
let project = Some(create_test_project("myapp"));
|
||||||
let prompt_with_project = build_prompt(false, None, &project);
|
let prompt_with_project = build_prompt(false, false, None, &project);
|
||||||
assert!(prompt_with_project.contains("myapp"));
|
assert!(prompt_with_project.contains("myapp"));
|
||||||
|
|
||||||
// Simulate /unproject (sets active_project to None)
|
// Simulate /unproject (sets active_project to None)
|
||||||
let prompt_after_unproject = build_prompt(false, None, &None);
|
let prompt_after_unproject = build_prompt(false, false, None, &None);
|
||||||
assert_eq!(prompt_after_unproject, "g3> ");
|
assert_eq!(prompt_after_unproject, "g3> ");
|
||||||
assert!(!prompt_after_unproject.contains("myapp"));
|
assert!(!prompt_after_unproject.contains("myapp"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_prompt_project_name_from_path() {
|
fn test_build_prompt_project_name_from_path() {
|
||||||
// Test that project name is extracted from path
|
|
||||||
let project = Some(Project {
|
let project = Some(Project {
|
||||||
path: PathBuf::from("/Users/dev/projects/awesome-app"),
|
path: PathBuf::from("/Users/dev/projects/awesome-app"),
|
||||||
content: "test".to_string(),
|
content: "test".to_string(),
|
||||||
loaded_files: vec![],
|
loaded_files: vec![],
|
||||||
});
|
});
|
||||||
let prompt = build_prompt(false, None, &project);
|
let prompt = build_prompt(false, false, None, &project);
|
||||||
assert!(prompt.contains("awesome-app"));
|
assert!(prompt.contains("awesome-app"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_approval_input() {
|
||||||
|
// Exact matches
|
||||||
|
assert!(is_approval_input("a"));
|
||||||
|
assert!(is_approval_input("approve"));
|
||||||
|
assert!(is_approval_input("approved"));
|
||||||
|
assert!(is_approval_input("yes"));
|
||||||
|
assert!(is_approval_input("y"));
|
||||||
|
assert!(is_approval_input("ok"));
|
||||||
|
|
||||||
|
// Case insensitive
|
||||||
|
assert!(is_approval_input("APPROVE"));
|
||||||
|
assert!(is_approval_input("Approved"));
|
||||||
|
|
||||||
|
// Misspellings
|
||||||
|
assert!(is_approval_input("approv"));
|
||||||
|
assert!(is_approval_input("aprove"));
|
||||||
|
assert!(is_approval_input("appprove"));
|
||||||
|
|
||||||
|
// Non-approval inputs
|
||||||
|
assert!(!is_approval_input("no"));
|
||||||
|
assert!(!is_approval_input("reject"));
|
||||||
|
assert!(!is_approval_input("hello world"));
|
||||||
|
|
||||||
|
// Should NOT match partial words in longer text
|
||||||
|
assert!(!is_approval_input("I want to approve this"));
|
||||||
|
assert!(!is_approval_input("please approve the plan"));
|
||||||
|
assert!(!is_approval_input("a]ppro ve")); // gibberish with 'a' at start
|
||||||
|
|
||||||
|
// Should match with trailing punctuation
|
||||||
|
assert!(is_approval_input("approved!"));
|
||||||
|
assert!(is_approval_input("approve."));
|
||||||
|
assert!(is_approval_input("yes!"));
|
||||||
|
assert!(is_approval_input("ok,"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user