mirror of
https://github.com/block/goose.git
synced 2026-04-28 03:29:36 +00:00
Make ctrl-c work like in claude code (#6900)
Some checks are pending
Canary / bundle-desktop-windows (push) Blocked by required conditions
Canary / Prepare Version (push) Waiting to run
Canary / build-cli (push) Blocked by required conditions
Canary / Upload Install Script (push) Blocked by required conditions
Canary / bundle-desktop (push) Blocked by required conditions
Canary / bundle-desktop-linux (push) Blocked by required conditions
Canary / Release (push) Blocked by required conditions
CI / changes (push) Waiting to run
CI / Check Rust Code Format (push) Blocked by required conditions
CI / Build and Test Rust Project (push) Blocked by required conditions
CI / Lint Rust Code (push) Blocked by required conditions
CI / Check OpenAPI Schema is Up-to-Date (push) Blocked by required conditions
CI / Test and Lint Electron Desktop App (push) Blocked by required conditions
Live Provider Tests / check-fork (push) Waiting to run
Live Provider Tests / changes (push) Blocked by required conditions
Live Provider Tests / Build Release Binary (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (Code Execution) (push) Blocked by required conditions
Publish Docker Image / docker (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Some checks are pending
Canary / bundle-desktop-windows (push) Blocked by required conditions
Canary / Prepare Version (push) Waiting to run
Canary / build-cli (push) Blocked by required conditions
Canary / Upload Install Script (push) Blocked by required conditions
Canary / bundle-desktop (push) Blocked by required conditions
Canary / bundle-desktop-linux (push) Blocked by required conditions
Canary / Release (push) Blocked by required conditions
CI / changes (push) Waiting to run
CI / Check Rust Code Format (push) Blocked by required conditions
CI / Build and Test Rust Project (push) Blocked by required conditions
CI / Lint Rust Code (push) Blocked by required conditions
CI / Check OpenAPI Schema is Up-to-Date (push) Blocked by required conditions
CI / Test and Lint Electron Desktop App (push) Blocked by required conditions
Live Provider Tests / check-fork (push) Waiting to run
Live Provider Tests / changes (push) Blocked by required conditions
Live Provider Tests / Build Release Binary (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (Code Execution) (push) Blocked by required conditions
Publish Docker Image / docker (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
parent
1373d9c5f9
commit
0f334dbd45
4 changed files with 86 additions and 24 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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,
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<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(
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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?",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue