mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
tui: avoid Esc interrupt when skill popup active (#9451)
Fixes #9450 ## What - When a task is running and the skills autocomplete popup is open, `Esc` now dismisses the popup instead of sending `Op::Interrupt`. - `Esc` still interrupts a running task when no popup is active. ## Tests - `cargo test -p codex-tui` --------- Co-authored-by: prateek <199982+prateek@users.noreply.github.com>
This commit is contained in:
@@ -243,8 +243,10 @@ impl BottomPane {
|
||||
} else {
|
||||
// If a task is running and a status line is visible, allow Esc to
|
||||
// send an interrupt even while the composer has focus.
|
||||
if matches!(key_event.code, crossterm::event::KeyCode::Esc)
|
||||
// When a popup is active, prefer dismissing it over interrupting the task.
|
||||
if key_event.code == KeyCode::Esc
|
||||
&& self.is_task_running
|
||||
&& !self.composer.popup_active()
|
||||
&& let Some(status) = &self.status
|
||||
{
|
||||
// Send Op::Interrupt
|
||||
@@ -696,9 +698,13 @@ impl Renderable for BottomPane {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app_event::AppEvent;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
fn snapshot_buffer(buf: &Buffer) -> String {
|
||||
@@ -965,4 +971,109 @@ mod tests {
|
||||
render_snapshot(&pane, area)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esc_with_skill_popup_does_not_interrupt_task() {
|
||||
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut pane = BottomPane::new(BottomPaneParams {
|
||||
app_event_tx: tx,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
has_input_focus: true,
|
||||
enhanced_keys_supported: false,
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
skills: Some(vec![SkillMetadata {
|
||||
name: "test-skill".to_string(),
|
||||
description: "test skill".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
path: PathBuf::from("test-skill"),
|
||||
scope: SkillScope::User,
|
||||
}]),
|
||||
});
|
||||
|
||||
pane.set_task_running(true);
|
||||
|
||||
// Repro: a running task + skill popup + Esc should dismiss the popup, not interrupt.
|
||||
pane.insert_str("$");
|
||||
assert!(
|
||||
pane.composer.popup_active(),
|
||||
"expected skill popup after typing `$`"
|
||||
);
|
||||
|
||||
pane.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
|
||||
while let Ok(ev) = rx.try_recv() {
|
||||
assert!(
|
||||
!matches!(ev, AppEvent::CodexOp(Op::Interrupt)),
|
||||
"expected Esc to not send Op::Interrupt when dismissing skill popup"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!pane.composer.popup_active(),
|
||||
"expected Esc to dismiss skill popup"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esc_with_slash_command_popup_does_not_interrupt_task() {
|
||||
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut pane = BottomPane::new(BottomPaneParams {
|
||||
app_event_tx: tx,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
has_input_focus: true,
|
||||
enhanced_keys_supported: false,
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
pane.set_task_running(true);
|
||||
|
||||
// Repro: a running task + slash-command popup + Esc should not interrupt the task.
|
||||
pane.insert_str("/");
|
||||
assert!(
|
||||
pane.composer.popup_active(),
|
||||
"expected command popup after typing `/`"
|
||||
);
|
||||
|
||||
pane.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
|
||||
while let Ok(ev) = rx.try_recv() {
|
||||
assert!(
|
||||
!matches!(ev, AppEvent::CodexOp(Op::Interrupt)),
|
||||
"expected Esc to not send Op::Interrupt while command popup is active"
|
||||
);
|
||||
}
|
||||
assert_eq!(pane.composer_text(), "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esc_interrupts_running_task_when_no_popup() {
|
||||
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut pane = BottomPane::new(BottomPaneParams {
|
||||
app_event_tx: tx,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
has_input_focus: true,
|
||||
enhanced_keys_supported: false,
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
pane.set_task_running(true);
|
||||
|
||||
pane.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
|
||||
assert!(
|
||||
matches!(rx.try_recv(), Ok(AppEvent::CodexOp(Op::Interrupt))),
|
||||
"expected Esc to send Op::Interrupt while a task is running"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user