Skip to content

Commit 9752ab9

Browse files
committed
handles sigint
1 parent fa0c30d commit 9752ab9

File tree

5 files changed

+85
-22
lines changed

5 files changed

+85
-22
lines changed

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

Lines changed: 40 additions & 10 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,27 @@ 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) {
307-
// TODO: fetch prompt related info from session and send it here
308331
if let Err(e) = prompt_signal_tx.send(prompt_signal.clone()) {
309332
error!("Error sending prompt signal: {:?}", e);
310333
}
311334
display_state = DisplayState::UserInsertingText;
335+
336+
ctrl_c_handler = Box::pin(future::pending());
337+
} else if ui_managed_ctrl_c {
338+
ctrl_c_handler = Box::pin(ctrl_c());
339+
} else {
340+
ctrl_c_handler = Box::pin(future::pending());
312341
}
313342

314343
tokio::select! {
344+
_ = ctrl_c_handler => {
345+
_ = self.sender.send(InputEvent::Interrupt).await;
346+
},
315347
Some(incoming_event) = incoming_events_rx.recv() => {
316348
match display_state {
317349
DisplayState::UserInsertingText => {
@@ -323,10 +355,8 @@ impl ViewEnd {
323355
display_state = DisplayState::StreamingOutput;
324356
},
325357
IncomingEvent::Interrupt => {
326-
// If user is still inputting text, the session does
327-
// not need to be notified that they are hitting
328-
// control c.
329358
display_state = DisplayState::default();
359+
_ = self.sender.send(InputEvent::Interrupt).await;
330360
},
331361
}
332362
},

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
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
@@ -645,7 +645,7 @@ impl ChatSession {
645645

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

@@ -1962,7 +1962,7 @@ impl ChatSession {
19621962
meta_type: "timing".to_string(),
19631963
payload: serde_json::Value::String("prompt_user".to_string()),
19641964
}))?;
1965-
self.read_user_input_managed().await
1965+
self.read_user_input_via_ui().await
19661966
} else {
19671967
let prompt = self.generate_tool_trust_prompt(os).await;
19681968
self.read_user_input(&prompt, false)
@@ -1973,7 +1973,11 @@ impl ChatSession {
19731973
};
19741974

19751975
// Check if there's a pending clipboard paste from Ctrl+V
1976-
let pasted_paths = self.input_source.take_clipboard_pastes();
1976+
let pasted_paths = self
1977+
.input_source
1978+
.as_mut()
1979+
.map(|input_source| input_source.take_clipboard_pastes())
1980+
.unwrap_or_default();
19771981
if !pasted_paths.is_empty() {
19781982
// Check if the input contains image markers
19791983
let image_marker_regex = regex::Regex::new(r"\[Image #\d+\]").unwrap();
@@ -1986,7 +1990,9 @@ impl ChatSession {
19861990
.join(" ");
19871991

19881992
// Reset the counter for next message
1989-
self.input_source.reset_paste_count();
1993+
if let Some(input_source) = self.input_source.as_mut() {
1994+
input_source.reset_paste_count();
1995+
}
19901996

19911997
// Return HandleInput with all paths to automatically process the images
19921998
return Ok(ChatState::HandleInput { input: paths_str });
@@ -1997,13 +2003,39 @@ impl ChatSession {
19972003
Ok(ChatState::HandleInput { input: user_input })
19982004
}
19992005

2000-
async fn read_user_input_managed(&mut self) -> Option<String> {
2006+
async fn read_user_input_via_ui(&mut self) -> Option<String> {
20012007
if let Some(managed_input) = &mut self.managed_input {
2002-
if let Some(content) = managed_input.recv().await {
2003-
if let InputEvent::Text(content) = content {
2004-
return Some(content);
2005-
} else {
2006-
return None;
2008+
let mut has_hit_ctrl_c = false;
2009+
while let Some(input_event) = managed_input.recv().await {
2010+
match input_event {
2011+
InputEvent::Text(content) => {
2012+
return Some(content);
2013+
},
2014+
InputEvent::Interrupt => {
2015+
if has_hit_ctrl_c {
2016+
return None;
2017+
} else {
2018+
has_hit_ctrl_c = true;
2019+
_ = execute!(
2020+
self.stderr,
2021+
style::Print(format!(
2022+
"\n(To exit the CLI, press Ctrl+C or Ctrl+D again or type {})\n\n",
2023+
"/quit".green()
2024+
))
2025+
);
2026+
2027+
if self
2028+
.stderr
2029+
.send(Event::MetaEvent(chat_cli_ui::protocol::MetaEvent {
2030+
meta_type: "timing".to_string(),
2031+
payload: serde_json::Value::String("prompt_user".to_string()),
2032+
}))
2033+
.is_err()
2034+
{
2035+
return None;
2036+
}
2037+
}
2038+
},
20072039
}
20082040
}
20092041
}

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)