diff --git a/Cargo.toml b/Cargo.toml index 6356c5a8b8a2..6ee2d01e6ff9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ wiremock = "0.6" serial_test = "3.2.0" test-case = "3.3.1" base64 = "0.22.1" -reqwest = { version = "0.12.28", default-features = false } +reqwest = { version = "0.12.28", default-features = false, features = ["multipart"] } tower = "0.5.2" tower-http = "0.6.8" url = "2.5.8" diff --git a/crates/goose-cli/src/session/completion.rs b/crates/goose-cli/src/session/completion.rs index 27f9592c0f4d..31cd86839d21 100644 --- a/crates/goose-cli/src/session/completion.rs +++ b/crates/goose-cli/src/session/completion.rs @@ -6,11 +6,11 @@ use rustyline::{Context, Helper, Result}; use std::borrow::Cow; use std::sync::Arc; -use super::CompletionCache; +use super::{CompletionCache, HintStatus}; /// Completer for goose CLI commands pub struct GooseCompleter { - completion_cache: Arc>, + pub completion_cache: Arc>, filename_completer: FilenameCompleter, } @@ -388,15 +388,33 @@ impl Hinter for GooseCompleter { type Hint = String; fn hint(&self, line: &str, _pos: usize, _ctx: &Context<'_>) -> Option { - // Only show hint when line is empty - if line.is_empty() { - let newline_key = super::input::get_newline_key().to_ascii_uppercase(); - Some(format!( - "Press Enter to send, Ctrl-{} for new line", - newline_key - )) - } else { - None + let cache = self.completion_cache.read().unwrap(); + + if !line.is_empty() && cache.hint_status != HintStatus::Default { + drop(cache); + let mut cache_write = self.completion_cache.write().unwrap(); + cache_write.hint_status = HintStatus::Default; + return None; + } + + if !line.is_empty() { + return None; + } + + match cache.hint_status { + HintStatus::Interrupted => { + Some("Interrupted, what should goose work on instead?".to_string()) + } + HintStatus::MaybeExit => { + Some("Press Ctrl+C again to exit, or type new instructions to continue".to_string()) + } + HintStatus::Default => { + let newline_key = super::input::get_newline_key().to_ascii_uppercase(); + Some(format!( + "Press Enter to send, Ctrl-{} for new line", + newline_key + )) + } } } } diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index 73ccee44b7c9..dc7227859a32 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -1,9 +1,11 @@ use super::completion::GooseCompleter; +use super::{CompletionCache, HintStatus}; use anyhow::Result; use goose::config::Config; use rustyline::Editor; use shlex; use std::collections::HashMap; +use std::sync::Arc; #[derive(Debug)] pub enum InputResult { @@ -37,10 +39,18 @@ pub struct PlanCommandOptions { pub message_text: String, } -struct CtrlCHandler; +struct CtrlCHandler { + completion_cache: Arc>, +} + +impl CtrlCHandler { + fn new(completion_cache: Arc>) -> Self { + Self { completion_cache } + } +} impl rustyline::ConditionalEventHandler for CtrlCHandler { - /// Handle Ctrl+C to clear the line if text is entered, otherwise exit the session. + /// Handle Ctrl+C to clear the line if text is entered, otherwise check if we should exit. fn handle( &self, _event: &rustyline::Event, @@ -49,9 +59,21 @@ impl rustyline::ConditionalEventHandler for CtrlCHandler { ctx: &rustyline::EventContext, ) -> Option { if !ctx.line().is_empty() { + // Clear the line if there's text + let mut cache = self.completion_cache.write().unwrap(); + cache.hint_status = HintStatus::Default; Some(rustyline::Cmd::Kill(rustyline::Movement::WholeBuffer)) } else { - Some(rustyline::Cmd::Interrupt) + let mut cache = self.completion_cache.write().unwrap(); + + if cache.hint_status == HintStatus::MaybeExit { + return Some(rustyline::Cmd::Interrupt); + } + + cache.hint_status = HintStatus::MaybeExit; + drop(cache); + + Some(rustyline::Cmd::Repaint) } } } @@ -83,6 +105,11 @@ pub fn get_input( return Ok(InputResult::Message(message)); } + let completion_cache = editor + .helper() + .map(|h| h.completion_cache.clone()) + .ok_or_else(|| anyhow::anyhow!("Editor helper not set"))?; + let newline_key = get_newline_key(); editor.bind_sequence( rustyline::KeyEvent( @@ -94,7 +121,7 @@ pub fn get_input( editor.bind_sequence( rustyline::KeyEvent(rustyline::KeyCode::Char('c'), rustyline::Modifiers::CTRL), - rustyline::EventHandler::Conditional(Box::new(CtrlCHandler)), + rustyline::EventHandler::Conditional(Box::new(CtrlCHandler::new(completion_cache))), ); let prompt = get_input_prompt_string(); @@ -136,10 +163,14 @@ pub fn get_input( } } -/// Get regular CLI input when editor mode doesn't have content fn get_regular_input( editor: &mut Editor, ) -> Result { + let completion_cache = editor + .helper() + .map(|h| h.completion_cache.clone()) + .ok_or_else(|| anyhow::anyhow!("Editor helper not set"))?; + let newline_key = get_newline_key(); editor.bind_sequence( rustyline::KeyEvent( @@ -151,7 +182,7 @@ fn get_regular_input( editor.bind_sequence( rustyline::KeyEvent(rustyline::KeyCode::Char('c'), rustyline::Modifiers::CTRL), - rustyline::EventHandler::Conditional(Box::new(CtrlCHandler)), + rustyline::EventHandler::Conditional(Box::new(CtrlCHandler::new(completion_cache))), ); let prompt = get_input_prompt_string(); diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 26447c5d78d4..7d49aed5d404 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -166,11 +166,19 @@ pub struct CliSession { output_format: String, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HintStatus { + Default, + Interrupted, + MaybeExit, +} + // Cache structure for completion data -struct CompletionCache { - prompts: HashMap>, - prompt_info: HashMap, - last_updated: Instant, +pub struct CompletionCache { + pub prompts: HashMap>, + pub prompt_info: HashMap, + pub last_updated: Instant, + pub hint_status: HintStatus, } impl CompletionCache { @@ -179,6 +187,7 @@ impl CompletionCache { prompts: HashMap::new(), prompt_info: HashMap::new(), last_updated: Instant::now(), + hint_status: HintStatus::Default, } } } @@ -1095,7 +1104,11 @@ impl CliSession { } async fn handle_interrupted_messages(&mut self, interrupt: bool) -> Result<()> { - // First, get any tool requests from the last message if it exists + if interrupt { + let mut cache = self.completion_cache.write().unwrap(); + cache.hint_status = HintStatus::Interrupted; + } + let tool_requests = self .messages .last() @@ -1116,6 +1129,7 @@ impl CliSession { if !tool_requests.is_empty() { // Interrupted during a tool request // Create tool responses for all interrupted tool requests + // TODO(Douwe): if we need this, it should happen in agent reply let mut response_message = Message::user(); let last_tool_name = tool_requests .last() @@ -1142,7 +1156,6 @@ impl CliSession { }), )); } - // TODO(Douwe): update also db self.push_message(response_message); let prompt = format!( "The existing call to {} was interrupted. How would you like to proceed?",