add ability to disable input temporarily in the TUI. (#8876)

We will disable input while the elevated sandbox setup is running.
This commit is contained in:
iceweasel-oai
2026-01-07 20:56:48 -08:00
committed by GitHub
parent 75076aabfe
commit ccba737d26
4 changed files with 166 additions and 4 deletions

View File

@@ -110,6 +110,9 @@ pub(crate) struct ChatComposer {
attached_images: Vec<AttachedImage>,
placeholder_text: String,
is_task_running: bool,
/// When false, the composer is temporarily read-only (e.g. during sandbox setup).
input_enabled: bool,
input_disabled_placeholder: Option<String>,
// Non-bracketed paste burst tracker.
paste_burst: PasteBurst,
// When true, disables paste-burst logic and inserts characters immediately.
@@ -160,6 +163,8 @@ impl ChatComposer {
attached_images: Vec::new(),
placeholder_text,
is_task_running: false,
input_enabled: true,
input_disabled_placeholder: None,
paste_burst: PasteBurst::default(),
disable_paste_burst: false,
custom_prompts: Vec::new(),
@@ -488,6 +493,10 @@ impl ChatComposer {
/// Handle a key event coming from the main UI.
pub fn handle_key_event(&mut self, key_event: KeyEvent) -> (InputResult, bool) {
if !self.input_enabled {
return (InputResult::None, false);
}
let result = match &mut self.active_popup {
ActivePopup::Command(_) => self.handle_key_event_with_slash_popup(key_event),
ActivePopup::File(_) => self.handle_key_event_with_file_popup(key_event),
@@ -1877,6 +1886,17 @@ impl ChatComposer {
self.has_focus = has_focus;
}
#[allow(dead_code)]
pub(crate) fn set_input_enabled(&mut self, enabled: bool, placeholder: Option<String>) {
self.input_enabled = enabled;
self.input_disabled_placeholder = if enabled { None } else { placeholder };
// Avoid leaving interactive popups open while input is blocked.
if !enabled && !matches!(self.active_popup, ActivePopup::None) {
self.active_popup = ActivePopup::None;
}
}
pub fn set_task_running(&mut self, running: bool) {
self.is_task_running = running;
}
@@ -1902,6 +1922,10 @@ impl ChatComposer {
impl Renderable for ChatComposer {
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
if !self.input_enabled {
return None;
}
let [_, textarea_rect, _] = self.layout_areas(area);
let state = *self.textarea_state.borrow();
self.textarea.cursor_pos_with_state(textarea_rect, state)
@@ -1980,10 +2004,15 @@ impl Renderable for ChatComposer {
let style = user_message_style();
Block::default().style(style).render_ref(composer_rect, buf);
if !textarea_rect.is_empty() {
let prompt = if self.input_enabled {
"".bold()
} else {
"".dim()
};
buf.set_span(
textarea_rect.x - LIVE_PREFIX_COLS,
textarea_rect.y,
&"".bold(),
&prompt,
textarea_rect.width,
);
}
@@ -1991,7 +2020,15 @@ impl Renderable for ChatComposer {
let mut state = self.textarea_state.borrow_mut();
StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state);
if self.textarea.text().is_empty() {
let placeholder = Span::from(self.placeholder_text.as_str()).dim();
let text = if self.input_enabled {
self.placeholder_text.as_str().to_string()
} else {
self.input_disabled_placeholder
.as_deref()
.unwrap_or("Input disabled.")
.to_string()
};
let placeholder = Span::from(text).dim().italic();
Line::from(vec![placeholder]).render_ref(textarea_rect.inner(Margin::new(0, 0)), buf);
}
}
@@ -4389,4 +4426,38 @@ mod tests {
);
assert_eq!(composer.attached_images.len(), 1);
}
#[test]
fn input_disabled_ignores_keypresses_and_hides_cursor() {
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
composer.set_text_content("hello".to_string());
composer.set_input_enabled(false, Some("Input disabled for test.".to_string()));
let (result, needs_redraw) =
composer.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
assert_eq!(result, InputResult::None);
assert!(!needs_redraw);
assert_eq!(composer.current_text(), "hello");
let area = Rect {
x: 0,
y: 0,
width: 40,
height: 5,
};
assert_eq!(composer.cursor_pos(area), None);
}
}

View File

@@ -264,6 +264,16 @@ impl BottomPane {
self.request_redraw();
}
#[allow(dead_code)]
pub(crate) fn set_composer_input_enabled(
&mut self,
enabled: bool,
placeholder: Option<String>,
) {
self.composer.set_input_enabled(enabled, placeholder);
self.request_redraw();
}
pub(crate) fn clear_composer_for_ctrl_c(&mut self) {
self.composer.clear_for_ctrl_c();
self.request_redraw();

View File

@@ -113,6 +113,9 @@ pub(crate) struct ChatComposer {
attached_images: Vec<AttachedImage>,
placeholder_text: String,
is_task_running: bool,
/// When false, the composer is temporarily read-only (e.g. during sandbox setup).
input_enabled: bool,
input_disabled_placeholder: Option<String>,
// Non-bracketed paste burst tracker.
paste_burst: PasteBurst,
// When true, disables paste-burst logic and inserts characters immediately.
@@ -168,6 +171,8 @@ impl ChatComposer {
attached_images: Vec::new(),
placeholder_text,
is_task_running: false,
input_enabled: true,
input_disabled_placeholder: None,
paste_burst: PasteBurst::default(),
disable_paste_burst: false,
custom_prompts: Vec::new(),
@@ -405,6 +410,10 @@ impl ChatComposer {
/// Handle a key event coming from the main UI.
pub fn handle_key_event(&mut self, key_event: KeyEvent) -> (InputResult, bool) {
if !self.input_enabled {
return (InputResult::None, false);
}
let result = match &mut self.active_popup {
ActivePopup::Command(_) => self.handle_key_event_with_slash_popup(key_event),
ActivePopup::File(_) => self.handle_key_event_with_file_popup(key_event),
@@ -1819,6 +1828,17 @@ impl ChatComposer {
self.has_focus = has_focus;
}
#[allow(dead_code)]
pub(crate) fn set_input_enabled(&mut self, enabled: bool, placeholder: Option<String>) {
self.input_enabled = enabled;
self.input_disabled_placeholder = if enabled { None } else { placeholder };
// Avoid leaving interactive popups open while input is blocked.
if !enabled && !matches!(self.active_popup, ActivePopup::None) {
self.active_popup = ActivePopup::None;
}
}
pub fn set_task_running(&mut self, running: bool) {
self.is_task_running = running;
}
@@ -1844,6 +1864,10 @@ impl ChatComposer {
impl Renderable for ChatComposer {
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
if !self.input_enabled {
return None;
}
let [_, textarea_rect, _] = self.layout_areas(area);
let state = *self.textarea_state.borrow();
self.textarea.cursor_pos_with_state(textarea_rect, state)
@@ -1922,10 +1946,15 @@ impl Renderable for ChatComposer {
let style = user_message_style();
Block::default().style(style).render_ref(composer_rect, buf);
if !textarea_rect.is_empty() {
let prompt = if self.input_enabled {
"".bold()
} else {
"".dim()
};
buf.set_span(
textarea_rect.x - LIVE_PREFIX_COLS,
textarea_rect.y,
&"".bold(),
&prompt,
textarea_rect.width,
);
}
@@ -1933,7 +1962,15 @@ impl Renderable for ChatComposer {
let mut state = self.textarea_state.borrow_mut();
StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state);
if self.textarea.text().is_empty() {
let placeholder = Span::from(self.placeholder_text.as_str()).dim();
let text = if self.input_enabled {
self.placeholder_text.as_str().to_string()
} else {
self.input_disabled_placeholder
.as_deref()
.unwrap_or("Input disabled.")
.to_string()
};
let placeholder = Span::from(text).dim().italic();
Line::from(vec![placeholder]).render_ref(textarea_rect.inner(Margin::new(0, 0)), buf);
}
}
@@ -4108,4 +4145,38 @@ mod tests {
"'/zzz' should not activate slash popup because it is not a prefix of any built-in command"
);
}
#[test]
fn input_disabled_ignores_keypresses_and_hides_cursor() {
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
composer.set_text_content("hello".to_string());
composer.set_input_enabled(false, Some("Input disabled for test.".to_string()));
let (result, needs_redraw) =
composer.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
assert_eq!(result, InputResult::None);
assert!(!needs_redraw);
assert_eq!(composer.current_text(), "hello");
let area = Rect {
x: 0,
y: 0,
width: 40,
height: 5,
};
assert_eq!(composer.cursor_pos(area), None);
}
}

View File

@@ -256,6 +256,16 @@ impl BottomPane {
self.request_redraw();
}
#[allow(dead_code)]
pub(crate) fn set_composer_input_enabled(
&mut self,
enabled: bool,
placeholder: Option<String>,
) {
self.composer.set_input_enabled(enabled, placeholder);
self.request_redraw();
}
pub(crate) fn clear_composer_for_ctrl_c(&mut self) {
self.composer.clear_for_ctrl_c();
self.request_redraw();