Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤨 seems unrelated to the rest?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm yeah, will revert

tower = "0.5.2"
tower-http = "0.6.8"
url = "2.5.8"
40 changes: 29 additions & 11 deletions crates/goose-cli/src/session/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::sync::RwLock<CompletionCache>>,
pub completion_cache: Arc<std::sync::RwLock<CompletionCache>>,
filename_completer: FilenameCompleter,
}

Expand Down Expand Up @@ -388,15 +388,33 @@ impl Hinter for GooseCompleter {
type Hint = String;

fn hint(&self, line: &str, _pos: usize, _ctx: &Context<'_>) -> Option<Self::Hint> {
// 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
))
}
}
}
}
Expand Down
43 changes: 37 additions & 6 deletions crates/goose-cli/src/session/input.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -37,10 +39,18 @@ pub struct PlanCommandOptions {
pub message_text: String,
}

struct CtrlCHandler;
struct CtrlCHandler {
completion_cache: Arc<std::sync::RwLock<CompletionCache>>,
}

impl CtrlCHandler {
fn new(completion_cache: Arc<std::sync::RwLock<CompletionCache>>) -> 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,
Expand All @@ -49,9 +59,21 @@ impl rustyline::ConditionalEventHandler for CtrlCHandler {
ctx: &rustyline::EventContext,
) -> Option<rustyline::Cmd> {
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)
}
}
}
Expand Down Expand Up @@ -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(
Expand All @@ -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();
Expand Down Expand Up @@ -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<GooseCompleter, rustyline::history::DefaultHistory>,
) -> Result<InputResult> {
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(
Expand All @@ -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();
Expand Down
25 changes: 19 additions & 6 deletions crates/goose-cli/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Vec<String>>,
prompt_info: HashMap<String, output::PromptInfo>,
last_updated: Instant,
pub struct CompletionCache {
pub prompts: HashMap<String, Vec<String>>,
pub prompt_info: HashMap<String, output::PromptInfo>,
pub last_updated: Instant,
pub hint_status: HintStatus,
}

impl CompletionCache {
Expand All @@ -179,6 +187,7 @@ impl CompletionCache {
prompts: HashMap::new(),
prompt_info: HashMap::new(),
last_updated: Instant::now(),
hint_status: HintStatus::Default,
}
}
}
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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?",
Expand Down
Loading