Skip to content

Commit fa0c30d

Browse files
committed
instrument current event loop to conditionally use managed input
1 parent 3b49e92 commit fa0c30d

File tree

3 files changed

+79
-37
lines changed

3 files changed

+79
-37
lines changed

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
@@ -4,7 +4,10 @@ use spinners::{
44
};
55

66
use crate::theme::StyledText;
7-
use crate::util::ui::should_send_structured_message;
7+
use crate::util::ui::{
8+
should_send_structured_message,
9+
should_use_ui_managed_input,
10+
};
811
pub mod cli;
912
mod consts;
1013
pub mod context;
@@ -429,13 +432,18 @@ impl ChatArgs {
429432
.build(os, Box::new(std::io::stderr()), !self.no_interactive)
430433
.await?;
431434
let tool_config = tool_manager.load_tools(os, &mut stderr).await?;
435+
let input_source = if should_use_ui_managed_input() {
436+
None
437+
} else {
438+
Some(InputSource::new(os, prompt_request_sender, prompt_response_receiver)?)
439+
};
432440

433441
ChatSession::new(
434442
os,
435443
&conversation_id,
436444
agents,
437445
input,
438-
InputSource::new(os, prompt_request_sender, prompt_response_receiver)?,
446+
input_source,
439447
self.resume,
440448
|| terminal::window_size().map(|s| s.columns.into()).ok(),
441449
tool_manager,
@@ -578,7 +586,7 @@ pub struct ChatSession {
578586
initial_input: Option<String>,
579587
/// Whether we're starting a new conversation or continuing an old one.
580588
existing_conversation: bool,
581-
input_source: InputSource,
589+
input_source: Option<InputSource>,
582590
/// Width of the terminal, required for [ParseState].
583591
terminal_width_provider: fn() -> Option<usize>,
584592
spinner: Option<Spinner>,
@@ -617,7 +625,7 @@ impl ChatSession {
617625
conversation_id: &str,
618626
mut agents: Agents,
619627
mut input: Option<String>,
620-
input_source: InputSource,
628+
input_source: Option<InputSource>,
621629
resume_conversation: bool,
622630
terminal_width_provider: fn() -> Option<usize>,
623631
tool_manager: ToolManager,
@@ -630,13 +638,14 @@ impl ChatSession {
630638
// Only load prior conversation if we need to resume
631639
let mut existing_conversation = false;
632640

641+
let should_use_ui_managed_input = input_source.is_none();
633642
let should_send_structured_msg = should_send_structured_message(os);
634643
let (view_end, managed_input, mut control_end_stderr, control_end_stdout) =
635644
get_legacy_conduits(should_send_structured_msg);
636645

637646
let stderr = std::io::stderr();
638647
let stdout = std::io::stdout();
639-
if let Err(e) = view_end.into_legacy_mode(true, StyledText, stderr, stdout) {
648+
if let Err(e) = view_end.into_legacy_mode(should_use_ui_managed_input, StyledText, stderr, stdout) {
640649
error!("Conduit view end legacy mode exited: {:?}", e);
641650
}
642651

@@ -740,7 +749,11 @@ impl ChatSession {
740749
inner: Some(ChatState::default()),
741750
ctrlc_rx,
742751
wrap,
743-
managed_input: Some(managed_input),
752+
managed_input: if should_use_ui_managed_input {
753+
Some(managed_input)
754+
} else {
755+
None
756+
},
744757
})
745758
}
746759

@@ -1938,8 +1951,9 @@ impl ChatSession {
19381951
.filter(|name| *name != DUMMY_TOOL_NAME)
19391952
.cloned()
19401953
.collect::<Vec<_>>();
1941-
self.input_source
1942-
.put_skim_command_selector(os, Arc::new(context_manager.clone()), tool_names);
1954+
if let Some(input_source) = &mut self.input_source {
1955+
input_source.put_skim_command_selector(os, Arc::new(context_manager.clone()), tool_names);
1956+
}
19431957
}
19441958

19451959
execute!(self.stderr, StyledText::reset(), StyledText::reset_attributes())?;
@@ -3438,9 +3452,14 @@ impl ChatSession {
34383452

34393453
/// Helper function to read user input with a prompt and Ctrl+C handling
34403454
fn read_user_input(&mut self, prompt: &str, exit_on_single_ctrl_c: bool) -> Option<String> {
3455+
// If this function is called at all, input_source should not be None
3456+
debug_assert!(self.input_source.is_some());
3457+
34413458
let mut ctrl_c = false;
3459+
let input_source = self.input_source.as_mut()?;
3460+
34423461
loop {
3443-
match (self.input_source.read_line(Some(prompt)), ctrl_c) {
3462+
match (input_source.read_line(Some(prompt)), ctrl_c) {
34443463
(Ok(Some(line)), _) => {
34453464
if line.trim().is_empty() {
34463465
continue; // Reprompt if the input is empty
@@ -3912,11 +3931,11 @@ mod tests {
39123931
"fake_conv_id",
39133932
agents,
39143933
None,
3915-
InputSource::new_mock(vec![
3934+
Some(InputSource::new_mock(vec![
39163935
"create a new file".to_string(),
39173936
"y".to_string(),
39183937
"exit".to_string(),
3919-
]),
3938+
])),
39203939
false,
39213940
|| Some(80),
39223941
tool_manager,
@@ -4040,7 +4059,7 @@ mod tests {
40404059
"fake_conv_id",
40414060
agents,
40424061
None,
4043-
InputSource::new_mock(vec![
4062+
Some(InputSource::new_mock(vec![
40444063
"/tools".to_string(),
40454064
"/tools help".to_string(),
40464065
"create a new file".to_string(),
@@ -4057,7 +4076,7 @@ mod tests {
40574076
"create a file".to_string(), // prompt again due to reset
40584077
"n".to_string(), // cancel
40594078
"exit".to_string(),
4060-
]),
4079+
])),
40614080
false,
40624081
|| Some(80),
40634082
tool_manager,
@@ -4145,15 +4164,15 @@ mod tests {
41454164
"fake_conv_id",
41464165
agents,
41474166
None,
4148-
InputSource::new_mock(vec![
4167+
Some(InputSource::new_mock(vec![
41494168
"create 2 new files parallel".to_string(),
41504169
"t".to_string(),
41514170
"/tools reset".to_string(),
41524171
"create 2 new files parallel".to_string(),
41534172
"y".to_string(),
41544173
"y".to_string(),
41554174
"exit".to_string(),
4156-
]),
4175+
])),
41574176
false,
41584177
|| Some(80),
41594178
tool_manager,
@@ -4221,13 +4240,13 @@ mod tests {
42214240
"fake_conv_id",
42224241
agents,
42234242
None,
4224-
InputSource::new_mock(vec![
4243+
Some(InputSource::new_mock(vec![
42254244
"/tools trust-all".to_string(),
42264245
"create a new file".to_string(),
42274246
"/tools reset".to_string(),
42284247
"create a new file".to_string(),
42294248
"exit".to_string(),
4230-
]),
4249+
])),
42314250
false,
42324251
|| Some(80),
42334252
tool_manager,
@@ -4277,7 +4296,11 @@ mod tests {
42774296
"fake_conv_id",
42784297
agents,
42794298
None,
4280-
InputSource::new_mock(vec!["/subscribe".to_string(), "y".to_string(), "/quit".to_string()]),
4299+
Some(InputSource::new_mock(vec![
4300+
"/subscribe".to_string(),
4301+
"y".to_string(),
4302+
"/quit".to_string(),
4303+
])),
42814304
false,
42824305
|| Some(80),
42834306
tool_manager,
@@ -4380,11 +4403,11 @@ mod tests {
43804403
"fake_conv_id",
43814404
agents,
43824405
None, // No initial input
4383-
InputSource::new_mock(vec![
4406+
Some(InputSource::new_mock(vec![
43844407
"read /test.txt".to_string(),
43854408
"y".to_string(), // Accept tool execution
43864409
"exit".to_string(),
4387-
]),
4410+
])),
43884411
false,
43894412
|| Some(80),
43904413
tool_manager,
@@ -4514,7 +4537,10 @@ mod tests {
45144537
"test_conv_id",
45154538
agents,
45164539
None,
4517-
InputSource::new_mock(vec!["read /sensitive.txt".to_string(), "exit".to_string()]),
4540+
Some(InputSource::new_mock(vec![
4541+
"read /sensitive.txt".to_string(),
4542+
"exit".to_string(),
4543+
])),
45184544
false,
45194545
|| Some(80),
45204546
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)