New feature: External editor such as vim, helix, etc (#5823)
Signed-off-by: Peter Willemsen <peter@codebuffet.co> Signed-off-by: = <peter@codebuffet.co>
This commit is contained in:
@@ -0,0 +1,399 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
use tempfile::Builder;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
/// Create temporary markdown file with conversation history
|
||||||
|
fn create_temp_file(messages: &[&str]) -> Result<NamedTempFile> {
|
||||||
|
let temp_file = Builder::new()
|
||||||
|
.prefix("goose_prompt_")
|
||||||
|
.suffix(".md")
|
||||||
|
.tempfile()?;
|
||||||
|
let mut content = String::from("# Goose Prompt Editor\n\n");
|
||||||
|
|
||||||
|
content.push_str("# Your prompt:\n\n");
|
||||||
|
|
||||||
|
if !messages.is_empty() {
|
||||||
|
content.push_str("# Recent conversation for context (newest first):\n\n");
|
||||||
|
for message in messages.iter().rev() {
|
||||||
|
content.push_str(&format!("{}\n", message));
|
||||||
|
}
|
||||||
|
content.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::write(temp_file.path(), content)?;
|
||||||
|
Ok(temp_file)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RAII guard to ensure symlink is cleaned up even on panic
|
||||||
|
struct SymlinkCleanup {
|
||||||
|
symlink_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymlinkCleanup {
|
||||||
|
fn new(symlink_path: PathBuf) -> Self {
|
||||||
|
Self { symlink_path }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SymlinkCleanup {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Always try to clean up the symlink, ignoring any errors
|
||||||
|
let _ = std::fs::remove_file(&self.symlink_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Launch editor and wait for completion
|
||||||
|
fn launch_editor(editor_cmd: &str, file_path: &PathBuf) -> Result<()> {
|
||||||
|
use std::process::Stdio;
|
||||||
|
|
||||||
|
let parts: Vec<&str> = editor_cmd.split_whitespace().collect();
|
||||||
|
if parts.is_empty() {
|
||||||
|
return Err(anyhow::anyhow!("Empty editor command"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cmd = Command::new(parts[0]);
|
||||||
|
if let Ok(cwd) = std::env::current_dir() {
|
||||||
|
cmd.current_dir(cwd);
|
||||||
|
}
|
||||||
|
if parts.len() > 1 {
|
||||||
|
cmd.args(&parts[1..]);
|
||||||
|
}
|
||||||
|
cmd.arg(file_path)
|
||||||
|
.stdin(Stdio::inherit())
|
||||||
|
.stdout(Stdio::inherit())
|
||||||
|
.stderr(Stdio::inherit());
|
||||||
|
|
||||||
|
let status = cmd.status()?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Editor exited with non-zero status: {}",
|
||||||
|
status.code().unwrap_or(-1)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main function to get input from editor
|
||||||
|
pub fn get_editor_input(editor_cmd: &str, messages: &[&str]) -> Result<(String, bool)> {
|
||||||
|
let temp_file = create_temp_file(messages)?;
|
||||||
|
let temp_path = temp_file.path().to_path_buf();
|
||||||
|
|
||||||
|
let symlink_path = PathBuf::from(".goose_prompt_temp.md");
|
||||||
|
|
||||||
|
if symlink_path.exists() {
|
||||||
|
std::fs::remove_file(&symlink_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
std::os::unix::fs::symlink(&temp_path, &symlink_path)?;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
std::os::windows::fs::symlink_file(&temp_path, &symlink_path)?;
|
||||||
|
|
||||||
|
let _cleanup_guard = SymlinkCleanup::new(symlink_path.clone());
|
||||||
|
|
||||||
|
let _original_template = {
|
||||||
|
let mut template_content = String::from("# Goose Prompt Editor\n\n");
|
||||||
|
template_content.push_str("# Your prompt:\n\n");
|
||||||
|
if !messages.is_empty() {
|
||||||
|
template_content.push_str("# Recent conversation for context (newest first):\n\n");
|
||||||
|
for message in messages.iter().rev() {
|
||||||
|
template_content.push_str(&format!("{}\n", message));
|
||||||
|
}
|
||||||
|
template_content.push('\n');
|
||||||
|
}
|
||||||
|
template_content
|
||||||
|
};
|
||||||
|
|
||||||
|
launch_editor(editor_cmd, &symlink_path)?;
|
||||||
|
|
||||||
|
let mut content = String::new();
|
||||||
|
let mut file = std::fs::File::open(&symlink_path)?;
|
||||||
|
file.read_to_string(&mut content)?;
|
||||||
|
|
||||||
|
let user_input = extract_user_input(&content);
|
||||||
|
|
||||||
|
let has_meaningful_content = !user_input.trim().is_empty();
|
||||||
|
|
||||||
|
Ok((user_input, has_meaningful_content))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract only the user's input from the markdown file
|
||||||
|
fn extract_user_input(content: &str) -> String {
|
||||||
|
if let Some(start) = content.find("# Your prompt:") {
|
||||||
|
let marker_len = "# Your prompt:".len();
|
||||||
|
#[allow(clippy::string_slice)]
|
||||||
|
let user_section = &content[start + marker_len..];
|
||||||
|
|
||||||
|
let end_patterns = [
|
||||||
|
"# Recent conversation for context",
|
||||||
|
"# Recent conversation for context (newest first):",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut end_pos = None;
|
||||||
|
for pattern in &end_patterns {
|
||||||
|
if let Some(pos) = user_section.find(pattern) {
|
||||||
|
end_pos = Some(pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_input_section = match end_pos {
|
||||||
|
Some(pos) =>
|
||||||
|
{
|
||||||
|
#[allow(clippy::string_slice)]
|
||||||
|
&user_section[..pos]
|
||||||
|
}
|
||||||
|
None => user_section,
|
||||||
|
};
|
||||||
|
|
||||||
|
user_input_section.trim().to_string()
|
||||||
|
} else {
|
||||||
|
content.trim().to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_user_input_with_editor_output() {
|
||||||
|
let content = r#"# Goose Prompt Editor
|
||||||
|
|
||||||
|
# Your prompt:
|
||||||
|
This is the hardcoded prompt response
|
||||||
|
# Recent conversation for context (newest first):
|
||||||
|
|
||||||
|
## User: Hello
|
||||||
|
## Assistant: Hi there!
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let result = extract_user_input(content);
|
||||||
|
|
||||||
|
assert_eq!(result, "This is the hardcoded prompt response");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_user_input_no_marker() {
|
||||||
|
let content = "Just plain text without markers";
|
||||||
|
let result = extract_user_input(content);
|
||||||
|
assert_eq!(result, "Just plain text without markers");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_user_input_conversation_history_heading() {
|
||||||
|
let content = r#"# Goose Prompt Editor
|
||||||
|
|
||||||
|
# Your prompt:
|
||||||
|
This is the user's input
|
||||||
|
|
||||||
|
# Recent conversation for context (newest first):
|
||||||
|
|
||||||
|
## User: Previous message
|
||||||
|
## Assistant: Previous response
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let result = extract_user_input(content);
|
||||||
|
assert_eq!(result, "This is the user's input");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_temp_file_with_messages() {
|
||||||
|
let messages = vec!["## User: Hello", "## Assistant: Hi there!"];
|
||||||
|
|
||||||
|
let temp_file = create_temp_file(&messages).unwrap();
|
||||||
|
let path = temp_file.path();
|
||||||
|
|
||||||
|
assert!(path.exists());
|
||||||
|
assert!(path.to_str().unwrap().contains("goose_prompt_"));
|
||||||
|
assert!(path.to_str().unwrap().ends_with(".md"));
|
||||||
|
|
||||||
|
let content = fs::read_to_string(path).unwrap();
|
||||||
|
assert!(content.contains("# Goose Prompt Editor"));
|
||||||
|
assert!(content.contains("## User: Hello"));
|
||||||
|
assert!(content.contains("## Assistant: Hi there!"));
|
||||||
|
assert!(content.contains("# Your prompt:"));
|
||||||
|
assert!(content.contains("# Recent conversation for context (newest first):"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_temp_file_with_prefix_suffix() {
|
||||||
|
let temp_file = Builder::new()
|
||||||
|
.prefix("goose_test_")
|
||||||
|
.suffix(".md")
|
||||||
|
.tempfile()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let name = temp_file.path().file_name().unwrap().to_str().unwrap();
|
||||||
|
assert!(name.starts_with("goose_test_"));
|
||||||
|
assert!(name.ends_with(".md"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_user_input() {
|
||||||
|
let content = r#"# Goose Prompt Editor
|
||||||
|
|
||||||
|
# Recent conversation for context:
|
||||||
|
|
||||||
|
# Your prompt:
|
||||||
|
This is the user's actual input
|
||||||
|
with multiple lines.
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let result = extract_user_input(content);
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
"This is the user's actual input\nwith multiple lines."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tempfile_cleanup() {
|
||||||
|
let path = {
|
||||||
|
let temp_file = Builder::new()
|
||||||
|
.prefix("goose_cleanup_test_")
|
||||||
|
.tempfile()
|
||||||
|
.unwrap();
|
||||||
|
let path = temp_file.path().to_path_buf();
|
||||||
|
assert!(path.exists());
|
||||||
|
path
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(!path.exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_message_ordering_newest_first() {
|
||||||
|
let messages = vec![
|
||||||
|
"## User: First message",
|
||||||
|
"## Assistant: First response",
|
||||||
|
"## User: Second message",
|
||||||
|
"## Assistant: Second response",
|
||||||
|
"## User: Third message (newest)",
|
||||||
|
];
|
||||||
|
|
||||||
|
let temp_file = create_temp_file(&messages).unwrap();
|
||||||
|
let content = fs::read_to_string(temp_file.path()).unwrap();
|
||||||
|
|
||||||
|
let newest_first = [
|
||||||
|
"## User: Third message (newest)",
|
||||||
|
"## Assistant: Second response",
|
||||||
|
"## User: Second message",
|
||||||
|
"## Assistant: First response",
|
||||||
|
"## User: First message",
|
||||||
|
];
|
||||||
|
|
||||||
|
for expected_msg in &newest_first {
|
||||||
|
assert!(
|
||||||
|
content.contains(expected_msg),
|
||||||
|
"Expected to find message '{}' in content",
|
||||||
|
expected_msg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let newest_pos = content.find("## User: Third message (newest)").unwrap();
|
||||||
|
let oldest_pos = content.find("## User: First message").unwrap();
|
||||||
|
assert!(
|
||||||
|
newest_pos < oldest_pos,
|
||||||
|
"Newest message should appear before oldest message"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_symlink_raii_cleanup_on_panic() {
|
||||||
|
use std::os::unix::fs;
|
||||||
|
use std::panic;
|
||||||
|
|
||||||
|
let messages = vec!["## User: Test message for panic cleanup"];
|
||||||
|
let temp_file = create_temp_file(&messages).unwrap();
|
||||||
|
let temp_path = temp_file.path().to_path_buf();
|
||||||
|
|
||||||
|
let symlink_path = PathBuf::from(format!("test_panic_cleanup_{}.md", std::process::id()));
|
||||||
|
|
||||||
|
if symlink_path.exists() {
|
||||||
|
let _ = std::fs::remove_file(&symlink_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!symlink_path.exists(),
|
||||||
|
"Symlink should not exist before test"
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fs::symlink(&temp_path, &symlink_path).unwrap();
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
std::os::windows::fs::symlink_file(&temp_path, &symlink_path).unwrap();
|
||||||
|
|
||||||
|
assert!(symlink_path.exists(), "Symlink should exist after creation");
|
||||||
|
|
||||||
|
let cleanup_guard = SymlinkCleanup::new(symlink_path.clone());
|
||||||
|
|
||||||
|
let result = panic::catch_unwind(|| {
|
||||||
|
let _guard = cleanup_guard;
|
||||||
|
panic!("Simulating a panic to test cleanup");
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(result.is_err(), "Panic should have been caught");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!symlink_path.exists(),
|
||||||
|
"Symlink should be cleaned up even after panic"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_symlink_creation_and_cleanup() {
|
||||||
|
use std::os::unix::fs;
|
||||||
|
|
||||||
|
let messages = vec!["## User: Test message"];
|
||||||
|
let temp_file = create_temp_file(&messages).unwrap();
|
||||||
|
let temp_path = temp_file.path().to_path_buf();
|
||||||
|
|
||||||
|
let symlink_path = PathBuf::from(format!("test_symlink_cleanup_{}.md", std::process::id()));
|
||||||
|
|
||||||
|
if symlink_path.exists() {
|
||||||
|
let _ = std::fs::remove_file(&symlink_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!symlink_path.exists(),
|
||||||
|
"Symlink should be removed before creating new one"
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fs::symlink(&temp_path, &symlink_path).unwrap();
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
std::os::windows::fs::symlink_file(&temp_path, &symlink_path).unwrap();
|
||||||
|
|
||||||
|
assert!(symlink_path.exists());
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(&symlink_path).unwrap();
|
||||||
|
assert!(content.contains("## User: Test message"));
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let read_link = std::fs::read_link(&symlink_path).unwrap();
|
||||||
|
assert_eq!(read_link, temp_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
assert!(temp_path.exists());
|
||||||
|
let temp_content = std::fs::read_to_string(&temp_path).unwrap();
|
||||||
|
assert_eq!(content, temp_content);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(&symlink_path);
|
||||||
|
assert!(!symlink_path.exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,6 +67,75 @@ pub fn get_newline_key() -> char {
|
|||||||
|
|
||||||
pub fn get_input(
|
pub fn get_input(
|
||||||
editor: &mut Editor<GooseCompleter, rustyline::history::DefaultHistory>,
|
editor: &mut Editor<GooseCompleter, rustyline::history::DefaultHistory>,
|
||||||
|
conversation_messages: Option<&Vec<String>>,
|
||||||
|
) -> Result<InputResult> {
|
||||||
|
let config = Config::global();
|
||||||
|
if let Ok(Some(editor_cmd)) = config.get_goose_prompt_editor() {
|
||||||
|
let messages = extract_recent_messages(conversation_messages);
|
||||||
|
let message_refs: Vec<&str> = messages.iter().map(|s| s.as_str()).collect();
|
||||||
|
let (message, has_meaningful_content) =
|
||||||
|
crate::session::editor::get_editor_input(&editor_cmd, &message_refs)?;
|
||||||
|
|
||||||
|
if !has_meaningful_content {
|
||||||
|
return get_regular_input(editor);
|
||||||
|
}
|
||||||
|
editor.add_history_entry(message.as_str())?;
|
||||||
|
return Ok(InputResult::Message(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure Ctrl-J binding is set for newlines
|
||||||
|
editor.bind_sequence(
|
||||||
|
rustyline::KeyEvent(rustyline::KeyCode::Char('j'), rustyline::Modifiers::CTRL),
|
||||||
|
rustyline::EventHandler::Simple(rustyline::Cmd::Newline),
|
||||||
|
);
|
||||||
|
|
||||||
|
editor.bind_sequence(
|
||||||
|
rustyline::KeyEvent(rustyline::KeyCode::Char('c'), rustyline::Modifiers::CTRL),
|
||||||
|
rustyline::EventHandler::Conditional(Box::new(CtrlCHandler)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let prompt = get_input_prompt_string();
|
||||||
|
|
||||||
|
let input = match editor.readline(&prompt) {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(e) => match e {
|
||||||
|
rustyline::error::ReadlineError::Interrupted => return Ok(InputResult::Exit),
|
||||||
|
rustyline::error::ReadlineError::Eof => return Ok(InputResult::Exit),
|
||||||
|
_ => return Err(e.into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add valid input to history (history saving to file is handled in the Session::interactive method)
|
||||||
|
if !input.trim().is_empty() {
|
||||||
|
editor.add_history_entry(input.as_str())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle non-slash commands first
|
||||||
|
if !input.starts_with('/') {
|
||||||
|
let trimmed = input.trim();
|
||||||
|
if trimmed.is_empty()
|
||||||
|
|| trimmed.eq_ignore_ascii_case("exit")
|
||||||
|
|| trimmed.eq_ignore_ascii_case("quit")
|
||||||
|
{
|
||||||
|
return Ok(if trimmed.is_empty() {
|
||||||
|
InputResult::Retry
|
||||||
|
} else {
|
||||||
|
InputResult::Exit
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Ok(InputResult::Message(trimmed.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle slash commands
|
||||||
|
match handle_slash_command(&input) {
|
||||||
|
Some(result) => Ok(result),
|
||||||
|
None => Ok(InputResult::Message(input.trim().to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get regular CLI input when editor mode doesn't have content
|
||||||
|
fn get_regular_input(
|
||||||
|
editor: &mut Editor<GooseCompleter, rustyline::history::DefaultHistory>,
|
||||||
) -> Result<InputResult> {
|
) -> Result<InputResult> {
|
||||||
let newline_key = get_newline_key();
|
let newline_key = get_newline_key();
|
||||||
editor.bind_sequence(
|
editor.bind_sequence(
|
||||||
@@ -142,6 +211,7 @@ fn handle_slash_command(input: &str) -> Option<InputResult> {
|
|||||||
"/exit" | "/quit" => Some(InputResult::Exit),
|
"/exit" | "/quit" => Some(InputResult::Exit),
|
||||||
"/?" | "/help" => {
|
"/?" | "/help" => {
|
||||||
print_help();
|
print_help();
|
||||||
|
print_editor_help();
|
||||||
Some(InputResult::Retry)
|
Some(InputResult::Retry)
|
||||||
}
|
}
|
||||||
"/t" => Some(InputResult::ToggleTheme),
|
"/t" => Some(InputResult::ToggleTheme),
|
||||||
@@ -341,6 +411,27 @@ Up/Down arrows - Navigate through command history"
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract recent messages for editor context
|
||||||
|
fn extract_recent_messages(conversation_messages: Option<&Vec<String>>) -> Vec<String> {
|
||||||
|
match conversation_messages {
|
||||||
|
Some(messages) => {
|
||||||
|
// Return the messages in reverse chronological order (newest first)
|
||||||
|
messages.clone()
|
||||||
|
}
|
||||||
|
None => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print help information about editor input
|
||||||
|
fn print_editor_help() {
|
||||||
|
println!(
|
||||||
|
"Editor Input:
|
||||||
|
When goose_prompt_editor is configured, prompts will open in your editor instead of the CLI.
|
||||||
|
Previous conversation is included as markdown headings for context.
|
||||||
|
Configure with: goose configure set goose_prompt_editor \"vim\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
mod builder;
|
mod builder;
|
||||||
mod completion;
|
mod completion;
|
||||||
|
mod editor;
|
||||||
mod elicitation;
|
mod elicitation;
|
||||||
mod export;
|
mod export;
|
||||||
mod input;
|
mod input;
|
||||||
@@ -444,7 +445,20 @@ impl CliSession {
|
|||||||
loop {
|
loop {
|
||||||
self.display_context_usage().await?;
|
self.display_context_usage().await?;
|
||||||
|
|
||||||
let input = input::get_input(&mut editor)?;
|
// Convert conversation messages to strings for editor mode
|
||||||
|
let conversation_strings: Vec<String> = self
|
||||||
|
.messages
|
||||||
|
.iter()
|
||||||
|
.map(|msg| {
|
||||||
|
let role = match msg.role {
|
||||||
|
rmcp::model::Role::User => "User",
|
||||||
|
rmcp::model::Role::Assistant => "Assistant",
|
||||||
|
};
|
||||||
|
format!("## {}: {}", role, msg.as_concat_text())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let input = input::get_input(&mut editor, Some(&conversation_strings))?;
|
||||||
if matches!(input, InputResult::Exit) {
|
if matches!(input, InputResult::Exit) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -957,6 +957,7 @@ config_value!(GOOSE_SEARCH_PATHS, Vec<String>);
|
|||||||
config_value!(GOOSE_MODE, GooseMode);
|
config_value!(GOOSE_MODE, GooseMode);
|
||||||
config_value!(GOOSE_PROVIDER, String);
|
config_value!(GOOSE_PROVIDER, String);
|
||||||
config_value!(GOOSE_MODEL, String);
|
config_value!(GOOSE_MODEL, String);
|
||||||
|
config_value!(GOOSE_PROMPT_EDITOR, Option<String>);
|
||||||
config_value!(GOOSE_MAX_ACTIVE_AGENTS, usize);
|
config_value!(GOOSE_MAX_ACTIVE_AGENTS, usize);
|
||||||
|
|
||||||
/// Load init-config.yaml from workspace root if it exists.
|
/// Load init-config.yaml from workspace root if it exists.
|
||||||
|
|||||||
Reference in New Issue
Block a user