Handle Ctrl+C quit when idle (#1402)
## Summary - show `Ctrl+C to quit` hint when pressing Ctrl+C with no active task - exiting with Ctrl+C if the hint is already visible - clear the hint when tasks begin or other keys are pressed https://github.com/user-attachments/assets/931e2d7c-1c80-4b45-9908-d119f74df23c ------ https://chatgpt.com/s/cd_685ec8875a308191beaa95886dc1379e Fixes #1245
This commit is contained in:
@@ -11,7 +11,6 @@ use crate::slash_command::SlashCommand;
|
|||||||
use crate::tui;
|
use crate::tui;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::protocol::Event;
|
use codex_core::protocol::Event;
|
||||||
use codex_core::protocol::Op;
|
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
@@ -193,10 +192,11 @@ impl<'a> App<'a> {
|
|||||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
// Forward interrupt to ChatWidget when active.
|
|
||||||
match &mut self.app_state {
|
match &mut self.app_state {
|
||||||
AppState::Chat { widget } => {
|
AppState::Chat { widget } => {
|
||||||
widget.submit_op(Op::Interrupt);
|
if widget.on_ctrl_c() {
|
||||||
|
self.app_event_tx.send(AppEvent::ExitRequest);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AppState::Login { .. } | AppState::GitWarning { .. } => {
|
AppState::Login { .. } | AppState::GitWarning { .. } => {
|
||||||
// No-op.
|
// No-op.
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ pub(crate) struct ChatComposer<'a> {
|
|||||||
command_popup: Option<CommandPopup>,
|
command_popup: Option<CommandPopup>,
|
||||||
app_event_tx: AppEventSender,
|
app_event_tx: AppEventSender,
|
||||||
history: ChatComposerHistory,
|
history: ChatComposerHistory,
|
||||||
|
ctrl_c_quit_hint: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatComposer<'_> {
|
impl ChatComposer<'_> {
|
||||||
@@ -51,6 +52,7 @@ impl ChatComposer<'_> {
|
|||||||
command_popup: None,
|
command_popup: None,
|
||||||
app_event_tx,
|
app_event_tx,
|
||||||
history: ChatComposerHistory::new(),
|
history: ChatComposerHistory::new(),
|
||||||
|
ctrl_c_quit_hint: false,
|
||||||
};
|
};
|
||||||
this.update_border(has_input_focus);
|
this.update_border(has_input_focus);
|
||||||
this
|
this
|
||||||
@@ -114,6 +116,11 @@ impl ChatComposer<'_> {
|
|||||||
self.update_border(has_focus);
|
self.update_border(has_focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_ctrl_c_quit_hint(&mut self, show: bool, has_focus: bool) {
|
||||||
|
self.ctrl_c_quit_hint = show;
|
||||||
|
self.update_border(has_focus);
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle a key event coming from the main UI.
|
/// Handle a key event coming from the main UI.
|
||||||
pub fn handle_key_event(&mut self, key_event: KeyEvent) -> (InputResult, bool) {
|
pub fn handle_key_event(&mut self, key_event: KeyEvent) -> (InputResult, bool) {
|
||||||
let result = match self.command_popup {
|
let result = match self.command_popup {
|
||||||
@@ -304,10 +311,17 @@ impl ChatComposer<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let bs = if has_focus {
|
let bs = if has_focus {
|
||||||
BlockState {
|
if self.ctrl_c_quit_hint {
|
||||||
right_title: Line::from("Enter to send | Ctrl+D to quit | Ctrl+J for newline")
|
BlockState {
|
||||||
.alignment(Alignment::Right),
|
right_title: Line::from("Ctrl+C to quit").alignment(Alignment::Right),
|
||||||
border_style: Style::default(),
|
border_style: Style::default(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
BlockState {
|
||||||
|
right_title: Line::from("Enter to send | Ctrl+D to quit | Ctrl+J for newline")
|
||||||
|
.alignment(Alignment::Right),
|
||||||
|
border_style: Style::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
BlockState {
|
BlockState {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub(crate) struct BottomPane<'a> {
|
|||||||
app_event_tx: AppEventSender,
|
app_event_tx: AppEventSender,
|
||||||
has_input_focus: bool,
|
has_input_focus: bool,
|
||||||
is_task_running: bool,
|
is_task_running: bool,
|
||||||
|
ctrl_c_quit_hint: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct BottomPaneParams {
|
pub(crate) struct BottomPaneParams {
|
||||||
@@ -52,6 +53,7 @@ impl BottomPane<'_> {
|
|||||||
app_event_tx: params.app_event_tx,
|
app_event_tx: params.app_event_tx,
|
||||||
has_input_focus: params.has_input_focus,
|
has_input_focus: params.has_input_focus,
|
||||||
is_task_running: false,
|
is_task_running: false,
|
||||||
|
ctrl_c_quit_hint: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +102,26 @@ impl BottomPane<'_> {
|
|||||||
self.composer.set_input_focus(has_focus);
|
self.composer.set_input_focus(has_focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn show_ctrl_c_quit_hint(&mut self) {
|
||||||
|
self.ctrl_c_quit_hint = true;
|
||||||
|
self.composer
|
||||||
|
.set_ctrl_c_quit_hint(true, self.has_input_focus);
|
||||||
|
self.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear_ctrl_c_quit_hint(&mut self) {
|
||||||
|
if self.ctrl_c_quit_hint {
|
||||||
|
self.ctrl_c_quit_hint = false;
|
||||||
|
self.composer
|
||||||
|
.set_ctrl_c_quit_hint(false, self.has_input_focus);
|
||||||
|
self.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn ctrl_c_quit_hint_visible(&self) -> bool {
|
||||||
|
self.ctrl_c_quit_hint
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_task_running(&mut self, running: bool) {
|
pub fn set_task_running(&mut self, running: bool) {
|
||||||
self.is_task_running = running;
|
self.is_task_running = running;
|
||||||
|
|
||||||
@@ -130,6 +152,10 @@ impl BottomPane<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_task_running(&self) -> bool {
|
||||||
|
self.is_task_running
|
||||||
|
}
|
||||||
|
|
||||||
/// Update the *context-window remaining* indicator in the composer. This
|
/// Update the *context-window remaining* indicator in the composer. This
|
||||||
/// is forwarded directly to the underlying `ChatComposer`.
|
/// is forwarded directly to the underlying `ChatComposer`.
|
||||||
pub(crate) fn set_token_usage(
|
pub(crate) fn set_token_usage(
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ impl ChatWidget<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handle_key_event(&mut self, key_event: KeyEvent) {
|
pub(crate) fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||||
|
self.bottom_pane.clear_ctrl_c_quit_hint();
|
||||||
// Special-case <Tab>: normally toggles focus between history and bottom panes.
|
// Special-case <Tab>: normally toggles focus between history and bottom panes.
|
||||||
// However, when the slash-command popup is visible we forward the key
|
// However, when the slash-command popup is visible we forward the key
|
||||||
// to the bottom pane so it can handle auto-completion.
|
// to the bottom pane so it can handle auto-completion.
|
||||||
@@ -244,6 +245,7 @@ impl ChatWidget<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
EventMsg::TaskStarted => {
|
EventMsg::TaskStarted => {
|
||||||
|
self.bottom_pane.clear_ctrl_c_quit_hint();
|
||||||
self.bottom_pane.set_task_running(true);
|
self.bottom_pane.set_task_running(true);
|
||||||
self.request_redraw();
|
self.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -402,6 +404,22 @@ impl ChatWidget<'_> {
|
|||||||
self.request_redraw();
|
self.request_redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle Ctrl-C key press.
|
||||||
|
/// Returns true if the key press was handled, false if it was not.
|
||||||
|
/// If the key press was not handled, the caller should handle it (likely by exiting the process).
|
||||||
|
pub(crate) fn on_ctrl_c(&mut self) -> bool {
|
||||||
|
if self.bottom_pane.is_task_running() {
|
||||||
|
self.bottom_pane.clear_ctrl_c_quit_hint();
|
||||||
|
self.submit_op(Op::Interrupt);
|
||||||
|
false
|
||||||
|
} else if self.bottom_pane.ctrl_c_quit_hint_visible() {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
self.bottom_pane.show_ctrl_c_quit_hint();
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Forward an `Op` directly to codex.
|
/// Forward an `Op` directly to codex.
|
||||||
pub(crate) fn submit_op(&self, op: Op) {
|
pub(crate) fn submit_op(&self, op: Op) {
|
||||||
if let Err(e) = self.codex_op_tx.send(op) {
|
if let Err(e) = self.codex_op_tx.send(op) {
|
||||||
|
|||||||
Reference in New Issue
Block a user