fix(cli): separate colored prefix from readline prompt
Rustyline's \x01...\x02 markers for ANSI codes didn't work correctly, causing cursor positioning issues and breaking line editing. New approach: build_prompt() returns (prefix, prompt) tuple where: - prefix: colored text printed before readline (contains ANSI codes) - prompt: plain text passed to readline (no ANSI codes) This ensures rustyline correctly calculates line length while still showing the colored project name.
This commit is contained in:
@@ -23,14 +23,14 @@ use crate::utils::display_context_progress;
|
|||||||
/// Build the interactive prompt string.
|
/// Build the interactive prompt string.
|
||||||
///
|
///
|
||||||
/// Format:
|
/// Format:
|
||||||
/// Note: ANSI escape codes are wrapped in \x01...\x02 markers for rustyline
|
/// Returns a tuple of (prefix_to_print, actual_prompt) where prefix_to_print
|
||||||
/// to correctly calculate visible prompt length (required for tab completion).
|
/// contains ANSI colors and should be printed before readline, and actual_prompt is plain text.
|
||||||
/// - Multiline mode: `"... > "`
|
/// - Multiline mode: `"... > "`
|
||||||
/// - No project: `"agent_name> "` (defaults to "g3")
|
/// - No project: `"agent_name> "` (defaults to "g3")
|
||||||
/// - With project: `"agent_name | project_name> "` where `| project_name>` is blue
|
/// - With project: `"agent_name | project_name> "` where `| project_name>` is blue
|
||||||
pub fn build_prompt(in_multiline: bool, agent_name: Option<&str>, active_project: &Option<Project>) -> String {
|
pub fn build_prompt(in_multiline: bool, agent_name: Option<&str>, active_project: &Option<Project>) -> (String, String) {
|
||||||
if in_multiline {
|
if in_multiline {
|
||||||
"... > ".to_string()
|
(String::new(), "... > ".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 {
|
||||||
@@ -38,18 +38,17 @@ pub fn build_prompt(in_multiline: bool, agent_name: Option<&str>, active_project
|
|||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|n| n.to_str())
|
.and_then(|n| n.to_str())
|
||||||
.unwrap_or("project");
|
.unwrap_or("project");
|
||||||
// Wrap ANSI codes in \x01...\x02 for rustyline to ignore them in length calculation
|
// Return colored prefix to print, and plain prompt for readline
|
||||||
let blue = format!("\x01{}\x02", SetForegroundColor(Color::Blue));
|
let prefix = format!(
|
||||||
let reset = format!("\x01{}\x02", ResetColor);
|
|
||||||
format!(
|
|
||||||
"{} {}| {}>{} ",
|
"{} {}| {}>{} ",
|
||||||
base_name,
|
base_name,
|
||||||
blue,
|
SetForegroundColor(Color::Blue),
|
||||||
project_name,
|
project_name,
|
||||||
reset
|
ResetColor
|
||||||
)
|
);
|
||||||
|
(prefix, String::new())
|
||||||
} else {
|
} else {
|
||||||
format!("{}> ", base_name)
|
(String::new(), format!("{}> ", base_name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,8 +183,16 @@ pub async fn run_interactive<W: UiWriter>(
|
|||||||
// Display context window progress bar before each prompt
|
// Display context window progress bar before each prompt
|
||||||
display_context_progress(&agent, &output);
|
display_context_progress(&agent, &output);
|
||||||
|
|
||||||
// Build prompt (shows project name in blue when active)
|
// Build prompt - returns (colored_prefix, plain_prompt)
|
||||||
let prompt = build_prompt(in_multiline, agent_name, &active_project);
|
// We print the colored prefix first, then use plain prompt for readline
|
||||||
|
// This avoids ANSI codes breaking rustyline's line length calculation
|
||||||
|
let (prefix, prompt) = build_prompt(in_multiline, agent_name, &active_project);
|
||||||
|
|
||||||
|
if !prefix.is_empty() {
|
||||||
|
use std::io::Write;
|
||||||
|
print!("{}", prefix);
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
}
|
||||||
|
|
||||||
let readline = rl.readline(&prompt);
|
let readline = rl.readline(&prompt);
|
||||||
match readline {
|
match readline {
|
||||||
@@ -336,62 +343,69 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_prompt_default() {
|
fn test_build_prompt_default() {
|
||||||
let prompt = build_prompt(false, None, &None);
|
let (prefix, prompt) = build_prompt(false, None, &None);
|
||||||
|
assert!(prefix.is_empty());
|
||||||
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 (prefix, prompt) = build_prompt(false, Some("butler"), &None);
|
||||||
|
assert!(prefix.is_empty());
|
||||||
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 (prefix, prompt) = build_prompt(true, None, &None);
|
||||||
|
assert!(prefix.is_empty());
|
||||||
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 (prefix, prompt) = build_prompt(true, Some("butler"), &None);
|
||||||
|
assert!(prefix.is_empty());
|
||||||
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 (prefix, prompt) = build_prompt(true, None, &project);
|
||||||
|
assert!(prefix.is_empty());
|
||||||
assert_eq!(prompt, "... > ");
|
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 (prefix, prompt) = build_prompt(false, None, &project);
|
||||||
// Should contain the project name in the prompt
|
// Project name should be in the colored prefix, prompt should be empty
|
||||||
assert!(prompt.contains("g3"));
|
assert!(prefix.contains("g3"));
|
||||||
assert!(prompt.contains("myapp"));
|
assert!(prefix.contains("myapp"));
|
||||||
assert!(prompt.contains("|"));
|
assert!(prefix.contains("|"));
|
||||||
|
assert!(prompt.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 (prefix, prompt) = build_prompt(false, Some("carmack"), &project);
|
||||||
// Should contain both agent name and project name
|
// Should contain both agent name and project name in prefix
|
||||||
assert!(prompt.contains("carmack"));
|
assert!(prefix.contains("carmack"));
|
||||||
assert!(prompt.contains("myapp"));
|
assert!(prefix.contains("myapp"));
|
||||||
assert!(prompt.contains("|"));
|
assert!(prefix.contains("|"));
|
||||||
|
assert!(prompt.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
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 (prefix_with_project, _) = build_prompt(false, None, &project);
|
||||||
assert!(prompt_with_project.contains("myapp"));
|
assert!(prefix_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 (prefix_after_unproject, prompt_after_unproject) = build_prompt(false, None, &None);
|
||||||
|
assert!(prefix_after_unproject.is_empty());
|
||||||
assert_eq!(prompt_after_unproject, "g3> ");
|
assert_eq!(prompt_after_unproject, "g3> ");
|
||||||
assert!(!prompt_after_unproject.contains("myapp"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -402,8 +416,9 @@ mod tests {
|
|||||||
content: "test".to_string(),
|
content: "test".to_string(),
|
||||||
loaded_files: vec![],
|
loaded_files: vec![],
|
||||||
});
|
});
|
||||||
let prompt = build_prompt(false, None, &project);
|
let (prefix, prompt) = build_prompt(false, None, &project);
|
||||||
assert!(prompt.contains("awesome-app"));
|
assert!(prefix.contains("awesome-app"));
|
||||||
|
assert!(prompt.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user