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:
prateek-oai
2026-01-19 15:52:04 -05:00
committed by GitHub
parent d544adf71a
commit 0c0c5aeddc

View File

@@ -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"
);
}
}