Skip to content

Commit f0e9cfb

Browse files
committed
handles sigint
1 parent be21c54 commit f0e9cfb

File tree

6 files changed

+86
-19
lines changed

6 files changed

+86
-19
lines changed

crates/chat-cli-ui/src/conduit.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use std::future;
12
use std::io::Write as _;
23
use std::marker::PhantomData;
34
use std::path::PathBuf;
5+
use std::pin::Pin;
46

57
use crossterm::style::{
68
self,
@@ -12,6 +14,7 @@ use crossterm::{
1214
queue,
1315
};
1416
use rustyline::EditMode;
17+
use tokio::signal::ctrl_c;
1518
use tracing::error;
1619

1720
use crate::legacy_ui_util::{
@@ -49,19 +52,36 @@ pub enum ConduitError {
4952
/// - To deliver state changes from the control layer to the view layer
5053
pub struct ViewEnd {
5154
/// Used by the view to send input to the control
52-
// TODO: later on we will need replace this byte array with an actual event type from ACP
5355
pub sender: tokio::sync::mpsc::Sender<InputEvent>,
5456
/// To receive messages from control about state changes
5557
pub receiver: tokio::sync::mpsc::UnboundedReceiver<Event>,
5658
}
5759

5860
impl ViewEnd {
59-
/// Method to facilitate in the interim
60-
/// It takes possible messages from the old even loop and queues write to the output provided
61-
/// This blocks the current thread and consumes the [ViewEnd]
61+
/// Converts the ViewEnd into legacy mode operation. This mainly serves a purpose in the
62+
/// following circumstances:
63+
/// - To preserve the UX of the current event loop while abstracting away the impl Write it
64+
/// writes to
65+
/// - To serve as an interim UI for the new event loop while preserving the UX of the current
66+
/// product while the new UI is being worked out
67+
///
68+
/// # Parameters
69+
///
70+
/// * `ui_managed_input` - When true, the UI layer will manage user input through readline. When
71+
/// false, input handling is delegated to the event loop (via InputSource).
72+
/// * `ui_managed_ctrl_c` - When true, the UI layer will handle Ctrl+C interrupts. When false,
73+
/// interrupt handling is delegated to the event loop (via its own ctrl c handler).
74+
/// * `theme_source` - Provider for terminal styling and theming information.
75+
/// * `stderr` - Standard error stream for error output.
76+
/// * `stdout` - Standard output stream for normal output.
77+
///
78+
/// # Returns
79+
///
80+
/// Returns `Ok(())` on successful initialization, or a `ConduitError` if setup fails.
6281
pub fn into_legacy_mode(
6382
mut self,
64-
managed_input: bool,
83+
ui_managed_input: bool,
84+
ui_managed_ctrl_c: bool,
6585
theme_source: impl ThemeSource,
6686
mut stderr: std::io::Stderr,
6787
mut stdout: std::io::Stdout,
@@ -254,7 +274,7 @@ impl ViewEnd {
254274
Ok::<(), ConduitError>(())
255275
}
256276

257-
if managed_input {
277+
if ui_managed_input {
258278
let (incoming_events_tx, mut incoming_events_rx) = tokio::sync::mpsc::unbounded_channel::<IncomingEvent>();
259279
let (prompt_signal_tx, prompt_signal_rx) = std::sync::mpsc::channel::<PromptSignal>();
260280

@@ -303,15 +323,28 @@ impl ViewEnd {
303323
let prompt_signal = PromptSignal::default();
304324

305325
loop {
326+
let ctrl_c_handler: Pin<
327+
Box<dyn Future<Output = Result<(), std::io::Error>> + Send + Sync + 'static>,
328+
>;
329+
306330
if matches!(display_state, DisplayState::Prompting) {
307331
// TODO: fetch prompt related info from session and send it here
308332
if let Err(e) = prompt_signal_tx.send(prompt_signal.clone()) {
309333
error!("Error sending prompt signal: {:?}", e);
310334
}
311335
display_state = DisplayState::UserInsertingText;
336+
337+
ctrl_c_handler = Box::pin(future::pending());
338+
} else if ui_managed_ctrl_c {
339+
ctrl_c_handler = Box::pin(ctrl_c());
340+
} else {
341+
ctrl_c_handler = Box::pin(future::pending());
312342
}
313343

314344
tokio::select! {
345+
_ = ctrl_c_handler => {
346+
_ = self.sender.send(InputEvent::Interrupt).await;
347+
},
315348
Some(incoming_event) = incoming_events_rx.recv() => {
316349
match display_state {
317350
DisplayState::UserInsertingText => {
@@ -327,6 +360,7 @@ impl ViewEnd {
327360
// not need to be notified that they are hitting
328361
// control c.
329362
display_state = DisplayState::default();
363+
_ = self.sender.send(InputEvent::Interrupt).await;
330364
},
331365
}
332366
},

crates/chat-cli-ui/src/legacy_ui_util.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ impl Completer for ChatCompleter {
123123
if line.starts_with('@') {
124124
let search_word = line.strip_prefix('@').unwrap_or("");
125125
// Here we assume that the names given by the event loop is already namespaced
126-
// approriately (i.e. not namespaced if the prompt name is unique and namespaced with
126+
// appropriately (i.e. not namespaced if the prompt name is unique and namespaced with
127127
// their respective server if it is)
128128
let completions = self
129129
.available_prompts

crates/chat-cli/src/cli/chat/custom_spinner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ impl Spinners {
2626
pb.set_style(
2727
ProgressStyle::default_spinner()
2828
.tick_chars(SPINNER_CHARS)
29-
.template("{spinner:.green} {msg}")
29+
.template("{spinner} {msg}")
3030
.unwrap(),
3131
);
3232
pb.set_message(message);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ impl ChatSession {
642642

643643
let stderr = std::io::stderr();
644644
let stdout = std::io::stdout();
645-
if let Err(e) = view_end.into_legacy_mode(should_use_ui_managed_input, StyledText, stderr, stdout) {
645+
if let Err(e) = view_end.into_legacy_mode(should_use_ui_managed_input, false, StyledText, stderr, stdout) {
646646
error!("Conduit view end legacy mode exited: {:?}", e);
647647
}
648648

@@ -1928,7 +1928,7 @@ impl ChatSession {
19281928
meta_type: "timing".to_string(),
19291929
payload: serde_json::Value::String("prompt_user".to_string()),
19301930
}))?;
1931-
self.read_user_input_managed().await
1931+
self.read_user_input_via_ui().await
19321932
} else {
19331933
let prompt = self.generate_tool_trust_prompt(os).await;
19341934
self.read_user_input(&prompt, false)
@@ -1939,7 +1939,11 @@ impl ChatSession {
19391939
};
19401940

19411941
// Check if there's a pending clipboard paste from Ctrl+V
1942-
let pasted_paths = self.input_source.take_clipboard_pastes();
1942+
let pasted_paths = self
1943+
.input_source
1944+
.as_mut()
1945+
.map(|input_source| input_source.take_clipboard_pastes())
1946+
.unwrap_or_default();
19431947
if !pasted_paths.is_empty() {
19441948
// Check if the input contains image markers
19451949
let image_marker_regex = regex::Regex::new(r"\[Image #\d+\]").unwrap();
@@ -1952,7 +1956,9 @@ impl ChatSession {
19521956
.join(" ");
19531957

19541958
// Reset the counter for next message
1955-
self.input_source.reset_paste_count();
1959+
if let Some(input_source) = self.input_source.as_mut() {
1960+
input_source.reset_paste_count();
1961+
}
19561962

19571963
// Return HandleInput with all paths to automatically process the images
19581964
return Ok(ChatState::HandleInput { input: paths_str });
@@ -1963,13 +1969,39 @@ impl ChatSession {
19631969
Ok(ChatState::HandleInput { input: user_input })
19641970
}
19651971

1966-
async fn read_user_input_managed(&mut self) -> Option<String> {
1972+
async fn read_user_input_via_ui(&mut self) -> Option<String> {
19671973
if let Some(managed_input) = &mut self.managed_input {
1968-
if let Some(content) = managed_input.recv().await {
1969-
if let InputEvent::Text(content) = content {
1970-
return Some(content);
1971-
} else {
1972-
return None;
1974+
let mut has_hit_ctrl_c = false;
1975+
while let Some(input_event) = managed_input.recv().await {
1976+
match input_event {
1977+
InputEvent::Text(content) => {
1978+
return Some(content);
1979+
},
1980+
InputEvent::Interrupt => {
1981+
if has_hit_ctrl_c {
1982+
return None;
1983+
} else {
1984+
has_hit_ctrl_c = true;
1985+
_ = execute!(
1986+
self.stderr,
1987+
style::Print(format!(
1988+
"\n(To exit the CLI, press Ctrl+C or Ctrl+D again or type {})\n\n",
1989+
"/quit".green()
1990+
))
1991+
);
1992+
1993+
if self
1994+
.stderr
1995+
.send(Event::MetaEvent(chat_cli_ui::protocol::MetaEvent {
1996+
meta_type: "timing".to_string(),
1997+
payload: serde_json::Value::String("prompt_user".to_string()),
1998+
}))
1999+
.is_err()
2000+
{
2001+
return None;
2002+
}
2003+
}
2004+
},
19732005
}
19742006
}
19752007
}

crates/chat-cli/src/util/ui.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ fn print_with_bold(output: &mut impl Write, segments: &[(String, bool)]) -> Resu
160160
/// - structured: This makes the event loop send structured messages where applicable (in addition
161161
/// to logging ANSI bytes directly where it has not been instrumented)
162162
/// - new: This spawns the new UI to be used on top of the current event loop (if we end up enabling
163-
/// this)
163+
/// this). This would also require the event loop to emit structured events.
164164
/// - unset: This is the default behavior where everything is unstructured (i.e. ANSI bytes straight
165165
/// to stderr or stdout)
166166
///

0 commit comments

Comments
 (0)