Skip to content

Commit be21c54

Browse files
committed
instrument current event loop to conditionally use managed input
1 parent c39358c commit be21c54

3 files changed

Lines changed: 79 additions & 37 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ impl ViewEnd {
6161
/// This blocks the current thread and consumes the [ViewEnd]
6262
pub fn into_legacy_mode(
6363
mut self,
64-
handle_input: bool,
64+
managed_input: bool,
6565
theme_source: impl ThemeSource,
6666
mut stderr: std::io::Stderr,
6767
mut stdout: std::io::Stdout,
@@ -254,7 +254,7 @@ impl ViewEnd {
254254
Ok::<(), ConduitError>(())
255255
}
256256

257-
if handle_input {
257+
if managed_input {
258258
let (incoming_events_tx, mut incoming_events_rx) = tokio::sync::mpsc::unbounded_channel::<IncomingEvent>();
259259
let (prompt_signal_tx, prompt_signal_rx) = std::sync::mpsc::channel::<PromptSignal>();
260260

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

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use crate::theme::StyledText;
2-
use crate::util::ui::should_send_structured_message;
2+
use crate::util::ui::{
3+
should_send_structured_message,
4+
should_use_ui_managed_input,
5+
};
36
pub mod cli;
47
mod consts;
58
pub mod context;
@@ -426,13 +429,18 @@ impl ChatArgs {
426429
.build(os, Box::new(std::io::stderr()), !self.no_interactive)
427430
.await?;
428431
let tool_config = tool_manager.load_tools(os, &mut stderr).await?;
432+
let input_source = if should_use_ui_managed_input() {
433+
None
434+
} else {
435+
Some(InputSource::new(os, prompt_request_sender, prompt_response_receiver)?)
436+
};
429437

430438
ChatSession::new(
431439
os,
432440
&conversation_id,
433441
agents,
434442
input,
435-
InputSource::new(os, prompt_request_sender, prompt_response_receiver)?,
443+
input_source,
436444
self.resume,
437445
|| terminal::window_size().map(|s| s.columns.into()).ok(),
438446
tool_manager,
@@ -575,7 +583,7 @@ pub struct ChatSession {
575583
initial_input: Option<String>,
576584
/// Whether we're starting a new conversation or continuing an old one.
577585
existing_conversation: bool,
578-
input_source: InputSource,
586+
input_source: Option<InputSource>,
579587
/// Width of the terminal, required for [ParseState].
580588
terminal_width_provider: fn() -> Option<usize>,
581589
spinner: Option<Spinners>,
@@ -614,7 +622,7 @@ impl ChatSession {
614622
conversation_id: &str,
615623
mut agents: Agents,
616624
mut input: Option<String>,
617-
input_source: InputSource,
625+
input_source: Option<InputSource>,
618626
resume_conversation: bool,
619627
terminal_width_provider: fn() -> Option<usize>,
620628
tool_manager: ToolManager,
@@ -627,13 +635,14 @@ impl ChatSession {
627635
// Only load prior conversation if we need to resume
628636
let mut existing_conversation = false;
629637

638+
let should_use_ui_managed_input = input_source.is_none();
630639
let should_send_structured_msg = should_send_structured_message(os);
631640
let (view_end, managed_input, mut control_end_stderr, control_end_stdout) =
632641
get_legacy_conduits(should_send_structured_msg);
633642

634643
let stderr = std::io::stderr();
635644
let stdout = std::io::stdout();
636-
if let Err(e) = view_end.into_legacy_mode(true, StyledText, stderr, stdout) {
645+
if let Err(e) = view_end.into_legacy_mode(should_use_ui_managed_input, StyledText, stderr, stdout) {
637646
error!("Conduit view end legacy mode exited: {:?}", e);
638647
}
639648

@@ -737,7 +746,11 @@ impl ChatSession {
737746
inner: Some(ChatState::default()),
738747
ctrlc_rx,
739748
wrap,
740-
managed_input: Some(managed_input),
749+
managed_input: if should_use_ui_managed_input {
750+
Some(managed_input)
751+
} else {
752+
None
753+
},
741754
})
742755
}
743756

@@ -1904,8 +1917,9 @@ impl ChatSession {
19041917
.filter(|name| *name != DUMMY_TOOL_NAME)
19051918
.cloned()
19061919
.collect::<Vec<_>>();
1907-
self.input_source
1908-
.put_skim_command_selector(os, Arc::new(context_manager.clone()), tool_names);
1920+
if let Some(input_source) = &mut self.input_source {
1921+
input_source.put_skim_command_selector(os, Arc::new(context_manager.clone()), tool_names);
1922+
}
19091923
}
19101924

19111925
execute!(self.stderr, StyledText::reset(), StyledText::reset_attributes())?;
@@ -3382,9 +3396,14 @@ impl ChatSession {
33823396

33833397
/// Helper function to read user input with a prompt and Ctrl+C handling
33843398
fn read_user_input(&mut self, prompt: &str, exit_on_single_ctrl_c: bool) -> Option<String> {
3399+
// If this function is called at all, input_source should not be None
3400+
debug_assert!(self.input_source.is_some());
3401+
33853402
let mut ctrl_c = false;
3403+
let input_source = self.input_source.as_mut()?;
3404+
33863405
loop {
3387-
match (self.input_source.read_line(Some(prompt)), ctrl_c) {
3406+
match (input_source.read_line(Some(prompt)), ctrl_c) {
33883407
(Ok(Some(line)), _) => {
33893408
if line.trim().is_empty() {
33903409
continue; // Reprompt if the input is empty
@@ -3856,11 +3875,11 @@ mod tests {
38563875
"fake_conv_id",
38573876
agents,
38583877
None,
3859-
InputSource::new_mock(vec![
3878+
Some(InputSource::new_mock(vec![
38603879
"create a new file".to_string(),
38613880
"y".to_string(),
38623881
"exit".to_string(),
3863-
]),
3882+
])),
38643883
false,
38653884
|| Some(80),
38663885
tool_manager,
@@ -3984,7 +4003,7 @@ mod tests {
39844003
"fake_conv_id",
39854004
agents,
39864005
None,
3987-
InputSource::new_mock(vec![
4006+
Some(InputSource::new_mock(vec![
39884007
"/tools".to_string(),
39894008
"/tools help".to_string(),
39904009
"create a new file".to_string(),
@@ -4001,7 +4020,7 @@ mod tests {
40014020
"create a file".to_string(), // prompt again due to reset
40024021
"n".to_string(), // cancel
40034022
"exit".to_string(),
4004-
]),
4023+
])),
40054024
false,
40064025
|| Some(80),
40074026
tool_manager,
@@ -4089,15 +4108,15 @@ mod tests {
40894108
"fake_conv_id",
40904109
agents,
40914110
None,
4092-
InputSource::new_mock(vec![
4111+
Some(InputSource::new_mock(vec![
40934112
"create 2 new files parallel".to_string(),
40944113
"t".to_string(),
40954114
"/tools reset".to_string(),
40964115
"create 2 new files parallel".to_string(),
40974116
"y".to_string(),
40984117
"y".to_string(),
40994118
"exit".to_string(),
4100-
]),
4119+
])),
41014120
false,
41024121
|| Some(80),
41034122
tool_manager,
@@ -4165,13 +4184,13 @@ mod tests {
41654184
"fake_conv_id",
41664185
agents,
41674186
None,
4168-
InputSource::new_mock(vec![
4187+
Some(InputSource::new_mock(vec![
41694188
"/tools trust-all".to_string(),
41704189
"create a new file".to_string(),
41714190
"/tools reset".to_string(),
41724191
"create a new file".to_string(),
41734192
"exit".to_string(),
4174-
]),
4193+
])),
41754194
false,
41764195
|| Some(80),
41774196
tool_manager,
@@ -4221,7 +4240,11 @@ mod tests {
42214240
"fake_conv_id",
42224241
agents,
42234242
None,
4224-
InputSource::new_mock(vec!["/subscribe".to_string(), "y".to_string(), "/quit".to_string()]),
4243+
Some(InputSource::new_mock(vec![
4244+
"/subscribe".to_string(),
4245+
"y".to_string(),
4246+
"/quit".to_string(),
4247+
])),
42254248
false,
42264249
|| Some(80),
42274250
tool_manager,
@@ -4324,11 +4347,11 @@ mod tests {
43244347
"fake_conv_id",
43254348
agents,
43264349
None, // No initial input
4327-
InputSource::new_mock(vec![
4350+
Some(InputSource::new_mock(vec![
43284351
"read /test.txt".to_string(),
43294352
"y".to_string(), // Accept tool execution
43304353
"exit".to_string(),
4331-
]),
4354+
])),
43324355
false,
43334356
|| Some(80),
43344357
tool_manager,
@@ -4458,7 +4481,10 @@ mod tests {
44584481
"test_conv_id",
44594482
agents,
44604483
None,
4461-
InputSource::new_mock(vec!["read /sensitive.txt".to_string(), "exit".to_string()]),
4484+
Some(InputSource::new_mock(vec![
4485+
"read /sensitive.txt".to_string(),
4486+
"exit".to_string(),
4487+
])),
44624488
false,
44634489
|| Some(80),
44644490
tool_manager,

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

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ use crossterm::style::{
66
Attribute,
77
};
88
use eyre::Result;
9-
use serde::{
10-
Deserialize,
11-
Serialize,
12-
};
139

1410
use crate::cli::feed::Feed;
1511
use crate::constants::ui_text;
@@ -158,17 +154,37 @@ fn print_with_bold(output: &mut impl Write, segments: &[(String, bool)]) -> Resu
158154
Ok(())
159155
}
160156

161-
#[derive(Default, Debug, Serialize, Deserialize)]
162-
#[serde(rename_all = "camelCase")]
163-
pub enum UiMode {
164-
#[default]
165-
Structured,
166-
Passthrough,
167-
New,
168-
}
169-
157+
/// This dictates the event loop's egress behavior. It controls what gets emitted to the UI from the
158+
/// event loop.
159+
/// There are three possible potent states:
160+
/// - structured: This makes the event loop send structured messages where applicable (in addition
161+
/// to logging ANSI bytes directly where it has not been instrumented)
162+
/// - new: This spawns the new UI to be used on top of the current event loop (if we end up enabling
163+
/// this)
164+
/// - unset: This is the default behavior where everything is unstructured (i.e. ANSI bytes straight
165+
/// to stderr or stdout)
166+
///
167+
/// The reason why this is a setting as opposed to managed input, which is controlled via an env
168+
/// var, is because the choice of UI is a user concern. Whereas managed input is purely a
169+
/// development concern.
170170
pub fn should_send_structured_message(os: &Os) -> bool {
171171
let ui_mode = os.database.settings.get_string(Setting::UiMode);
172172

173-
ui_mode.as_deref().is_some_and(|mode| mode == "structured")
173+
ui_mode
174+
.as_deref()
175+
.is_some_and(|mode| mode == "structured" || mode == "new")
176+
}
177+
178+
/// NOTE: unless you are doing testing work for the new UI, you likely would not need to worry
179+
/// about setting this environment variable.
180+
/// This dictates the event loop's ingress behavior. It controls how the event loop receives input
181+
/// from the user.
182+
/// A normal input refers to the use of [crate::cli::chat::InputSource], which is owned by
183+
/// the [crate::cli::chat::ChatSession]. It is not managed by the UI layer (nor is the UI even
184+
/// aware of its existence).
185+
/// Conversely, an "ui managed" input is one where stdin is managed by the UI layer. For the event
186+
/// loop, this effectively means forgoing the ownership of [crate::cli::chat::InputSource] (it is
187+
/// replaced by a None) and instead delegating the reading of user input to the UI layer.
188+
pub fn should_use_ui_managed_input() -> bool {
189+
std::env::var("Q_UI_MANAGED_INPUT").is_ok()
174190
}

0 commit comments

Comments
 (0)