Make ctrl-c work like in claude code (#6900)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
+1
-1
@@ -32,7 +32,7 @@ wiremock = "0.6"
|
|||||||
serial_test = "3.2.0"
|
serial_test = "3.2.0"
|
||||||
test-case = "3.3.1"
|
test-case = "3.3.1"
|
||||||
base64 = "0.22.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 = "0.5.2"
|
||||||
tower-http = "0.6.8"
|
tower-http = "0.6.8"
|
||||||
url = "2.5.8"
|
url = "2.5.8"
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ use rustyline::{Context, Helper, Result};
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::CompletionCache;
|
use super::{CompletionCache, HintStatus};
|
||||||
|
|
||||||
/// Completer for goose CLI commands
|
/// Completer for goose CLI commands
|
||||||
pub struct GooseCompleter {
|
pub struct GooseCompleter {
|
||||||
completion_cache: Arc<std::sync::RwLock<CompletionCache>>,
|
pub completion_cache: Arc<std::sync::RwLock<CompletionCache>>,
|
||||||
filename_completer: FilenameCompleter,
|
filename_completer: FilenameCompleter,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,15 +388,33 @@ impl Hinter for GooseCompleter {
|
|||||||
type Hint = String;
|
type Hint = String;
|
||||||
|
|
||||||
fn hint(&self, line: &str, _pos: usize, _ctx: &Context<'_>) -> Option<Self::Hint> {
|
fn hint(&self, line: &str, _pos: usize, _ctx: &Context<'_>) -> Option<Self::Hint> {
|
||||||
// Only show hint when line is empty
|
let cache = self.completion_cache.read().unwrap();
|
||||||
if line.is_empty() {
|
|
||||||
let newline_key = super::input::get_newline_key().to_ascii_uppercase();
|
if !line.is_empty() && cache.hint_status != HintStatus::Default {
|
||||||
Some(format!(
|
drop(cache);
|
||||||
"Press Enter to send, Ctrl-{} for new line",
|
let mut cache_write = self.completion_cache.write().unwrap();
|
||||||
newline_key
|
cache_write.hint_status = HintStatus::Default;
|
||||||
))
|
return None;
|
||||||
} else {
|
}
|
||||||
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
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use super::completion::GooseCompleter;
|
use super::completion::GooseCompleter;
|
||||||
|
use super::{CompletionCache, HintStatus};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use goose::config::Config;
|
use goose::config::Config;
|
||||||
use rustyline::Editor;
|
use rustyline::Editor;
|
||||||
use shlex;
|
use shlex;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum InputResult {
|
pub enum InputResult {
|
||||||
@@ -37,10 +39,18 @@ pub struct PlanCommandOptions {
|
|||||||
pub message_text: String,
|
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 {
|
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(
|
fn handle(
|
||||||
&self,
|
&self,
|
||||||
_event: &rustyline::Event,
|
_event: &rustyline::Event,
|
||||||
@@ -49,9 +59,21 @@ impl rustyline::ConditionalEventHandler for CtrlCHandler {
|
|||||||
ctx: &rustyline::EventContext,
|
ctx: &rustyline::EventContext,
|
||||||
) -> Option<rustyline::Cmd> {
|
) -> Option<rustyline::Cmd> {
|
||||||
if !ctx.line().is_empty() {
|
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))
|
Some(rustyline::Cmd::Kill(rustyline::Movement::WholeBuffer))
|
||||||
} else {
|
} 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));
|
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();
|
let newline_key = get_newline_key();
|
||||||
editor.bind_sequence(
|
editor.bind_sequence(
|
||||||
rustyline::KeyEvent(
|
rustyline::KeyEvent(
|
||||||
@@ -94,7 +121,7 @@ pub fn get_input(
|
|||||||
|
|
||||||
editor.bind_sequence(
|
editor.bind_sequence(
|
||||||
rustyline::KeyEvent(rustyline::KeyCode::Char('c'), rustyline::Modifiers::CTRL),
|
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();
|
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(
|
fn get_regular_input(
|
||||||
editor: &mut Editor<GooseCompleter, rustyline::history::DefaultHistory>,
|
editor: &mut Editor<GooseCompleter, rustyline::history::DefaultHistory>,
|
||||||
) -> Result<InputResult> {
|
) -> 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();
|
let newline_key = get_newline_key();
|
||||||
editor.bind_sequence(
|
editor.bind_sequence(
|
||||||
rustyline::KeyEvent(
|
rustyline::KeyEvent(
|
||||||
@@ -151,7 +182,7 @@ fn get_regular_input(
|
|||||||
|
|
||||||
editor.bind_sequence(
|
editor.bind_sequence(
|
||||||
rustyline::KeyEvent(rustyline::KeyCode::Char('c'), rustyline::Modifiers::CTRL),
|
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();
|
let prompt = get_input_prompt_string();
|
||||||
|
|||||||
@@ -166,11 +166,19 @@ pub struct CliSession {
|
|||||||
output_format: String,
|
output_format: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum HintStatus {
|
||||||
|
Default,
|
||||||
|
Interrupted,
|
||||||
|
MaybeExit,
|
||||||
|
}
|
||||||
|
|
||||||
// Cache structure for completion data
|
// Cache structure for completion data
|
||||||
struct CompletionCache {
|
pub struct CompletionCache {
|
||||||
prompts: HashMap<String, Vec<String>>,
|
pub prompts: HashMap<String, Vec<String>>,
|
||||||
prompt_info: HashMap<String, output::PromptInfo>,
|
pub prompt_info: HashMap<String, output::PromptInfo>,
|
||||||
last_updated: Instant,
|
pub last_updated: Instant,
|
||||||
|
pub hint_status: HintStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompletionCache {
|
impl CompletionCache {
|
||||||
@@ -179,6 +187,7 @@ impl CompletionCache {
|
|||||||
prompts: HashMap::new(),
|
prompts: HashMap::new(),
|
||||||
prompt_info: HashMap::new(),
|
prompt_info: HashMap::new(),
|
||||||
last_updated: Instant::now(),
|
last_updated: Instant::now(),
|
||||||
|
hint_status: HintStatus::Default,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1095,7 +1104,11 @@ impl CliSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_interrupted_messages(&mut self, interrupt: bool) -> Result<()> {
|
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
|
let tool_requests = self
|
||||||
.messages
|
.messages
|
||||||
.last()
|
.last()
|
||||||
@@ -1116,6 +1129,7 @@ impl CliSession {
|
|||||||
if !tool_requests.is_empty() {
|
if !tool_requests.is_empty() {
|
||||||
// Interrupted during a tool request
|
// Interrupted during a tool request
|
||||||
// Create tool responses for all interrupted tool requests
|
// 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 mut response_message = Message::user();
|
||||||
let last_tool_name = tool_requests
|
let last_tool_name = tool_requests
|
||||||
.last()
|
.last()
|
||||||
@@ -1142,7 +1156,6 @@ impl CliSession {
|
|||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// TODO(Douwe): update also db
|
|
||||||
self.push_message(response_message);
|
self.push_message(response_message);
|
||||||
let prompt = format!(
|
let prompt = format!(
|
||||||
"The existing call to {} was interrupted. How would you like to proceed?",
|
"The existing call to {} was interrupted. How would you like to proceed?",
|
||||||
|
|||||||
Reference in New Issue
Block a user