Skip to content

Commit c42d500

Browse files
committed
instrument current event loop to conditionally use managed input
1 parent 920f553 commit c42d500

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
@@ -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())?;
@@ -3361,9 +3375,14 @@ impl ChatSession {
33613375

33623376
/// Helper function to read user input with a prompt and Ctrl+C handling
33633377
fn read_user_input(&mut self, prompt: &str, exit_on_single_ctrl_c: bool) -> Option<String> {
3378+
// If this function is called at all, input_source should not be None
3379+
debug_assert!(self.input_source.is_some());
3380+
33643381
let mut ctrl_c = false;
3382+
let input_source = self.input_source.as_mut()?;
3383+
33653384
loop {
3366-
match (self.input_source.read_line(Some(prompt)), ctrl_c) {
3385+
match (input_source.read_line(Some(prompt)), ctrl_c) {
33673386
(Ok(Some(line)), _) => {
33683387
if line.trim().is_empty() {
33693388
continue; // Reprompt if the input is empty
@@ -3835,11 +3854,11 @@ mod tests {
38353854
"fake_conv_id",
38363855
agents,
38373856
None,
3838-
InputSource::new_mock(vec![
3857+
Some(InputSource::new_mock(vec![
38393858
"create a new file".to_string(),
38403859
"y".to_string(),
38413860
"exit".to_string(),
3842-
]),
3861+
])),
38433862
false,
38443863
|| Some(80),
38453864
tool_manager,
@@ -3963,7 +3982,7 @@ mod tests {
39633982
"fake_conv_id",
39643983
agents,
39653984
None,
3966-
InputSource::new_mock(vec![
3985+
Some(InputSource::new_mock(vec![
39673986
"/tools".to_string(),
39683987
"/tools help".to_string(),
39693988
"create a new file".to_string(),
@@ -3980,7 +3999,7 @@ mod tests {
39803999
"create a file".to_string(), // prompt again due to reset
39814000
"n".to_string(), // cancel
39824001
"exit".to_string(),
3983-
]),
4002+
])),
39844003
false,
39854004
|| Some(80),
39864005
tool_manager,
@@ -4068,15 +4087,15 @@ mod tests {
40684087
"fake_conv_id",
40694088
agents,
40704089
None,
4071-
InputSource::new_mock(vec![
4090+
Some(InputSource::new_mock(vec![
40724091
"create 2 new files parallel".to_string(),
40734092
"t".to_string(),
40744093
"/tools reset".to_string(),
40754094
"create 2 new files parallel".to_string(),
40764095
"y".to_string(),
40774096
"y".to_string(),
40784097
"exit".to_string(),
4079-
]),
4098+
])),
40804099
false,
40814100
|| Some(80),
40824101
tool_manager,
@@ -4144,13 +4163,13 @@ mod tests {
41444163
"fake_conv_id",
41454164
agents,
41464165
None,
4147-
InputSource::new_mock(vec![
4166+
Some(InputSource::new_mock(vec![
41484167
"/tools trust-all".to_string(),
41494168
"create a new file".to_string(),
41504169
"/tools reset".to_string(),
41514170
"create a new file".to_string(),
41524171
"exit".to_string(),
4153-
]),
4172+
])),
41544173
false,
41554174
|| Some(80),
41564175
tool_manager,
@@ -4200,7 +4219,11 @@ mod tests {
42004219
"fake_conv_id",
42014220
agents,
42024221
None,
4203-
InputSource::new_mock(vec!["/subscribe".to_string(), "y".to_string(), "/quit".to_string()]),
4222+
Some(InputSource::new_mock(vec![
4223+
"/subscribe".to_string(),
4224+
"y".to_string(),
4225+
"/quit".to_string(),
4226+
])),
42044227
false,
42054228
|| Some(80),
42064229
tool_manager,
@@ -4303,11 +4326,11 @@ mod tests {
43034326
"fake_conv_id",
43044327
agents,
43054328
None, // No initial input
4306-
InputSource::new_mock(vec![
4329+
Some(InputSource::new_mock(vec![
43074330
"read /test.txt".to_string(),
43084331
"y".to_string(), // Accept tool execution
43094332
"exit".to_string(),
4310-
]),
4333+
])),
43114334
false,
43124335
|| Some(80),
43134336
tool_manager,
@@ -4437,7 +4460,10 @@ mod tests {
44374460
"test_conv_id",
44384461
agents,
44394462
None,
4440-
InputSource::new_mock(vec!["read /sensitive.txt".to_string(), "exit".to_string()]),
4463+
Some(InputSource::new_mock(vec![
4464+
"read /sensitive.txt".to_string(),
4465+
"exit".to_string(),
4466+
])),
44414467
false,
44424468
|| Some(80),
44434469
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)