diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index e1dde833..33297ad3 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -199,7 +199,21 @@ impl<'a> App<'a> { modifiers: crossterm::event::KeyModifiers::CONTROL, .. } => { - self.app_event_tx.send(AppEvent::ExitRequest); + match &mut self.app_state { + AppState::Chat { widget } => { + if widget.composer_is_empty() { + self.app_event_tx.send(AppEvent::ExitRequest); + } else { + // Treat Ctrl+D as a normal key event when the composer + // is not empty so that it doesn't quit the application + // prematurely. + self.dispatch_key_event(key_event); + } + } + AppState::Login { .. } | AppState::GitWarning { .. } => { + self.app_event_tx.send(AppEvent::ExitRequest); + } + } } _ => { self.dispatch_key_event(key_event); diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index e89187d1..b49bce40 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -76,6 +76,11 @@ impl ChatComposer<'_> { this } + /// Returns true if the composer currently contains no user input. + pub(crate) fn is_empty(&self) -> bool { + self.textarea.is_empty() + } + /// Update the cached *context-left* percentage and refresh the placeholder /// text. The UI relies on the placeholder to convey the remaining /// context when the composer is empty. diff --git a/codex-rs/tui/src/bottom_pane/chat_composer_history.rs b/codex-rs/tui/src/bottom_pane/chat_composer_history.rs index fc85c282..5715c994 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer_history.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer_history.rs @@ -72,8 +72,7 @@ impl ChatComposerHistory { return false; } - let lines = textarea.lines(); - if lines.len() == 1 && lines[0].is_empty() { + if textarea.is_empty() { return true; } @@ -85,6 +84,7 @@ impl ChatComposerHistory { return false; } + let lines = textarea.lines(); matches!(&self.last_history_text, Some(prev) if prev == &lines.join("\n")) } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 350492b3..e4ea1d38 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -162,6 +162,10 @@ impl BottomPane<'_> { } } + pub(crate) fn composer_is_empty(&self) -> bool { + self.composer.is_empty() + } + pub(crate) fn is_task_running(&self) -> bool { self.is_task_running } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 865e3397..51fdfc3e 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -432,6 +432,10 @@ impl ChatWidget<'_> { } } + pub(crate) fn composer_is_empty(&self) -> bool { + self.bottom_pane.composer_is_empty() + } + /// Forward an `Op` directly to codex. pub(crate) fn submit_op(&self, op: Op) { if let Err(e) = self.codex_op_tx.send(op) {