Keep agent-switch word-motion keys out of draft editing (#14376)

## Summary
- only trigger multi-agent fast-switch shortcuts when the composer is
empty
- keep the Option+b/f fallback for terminals that encode Option+arrow
that way
- document why the empty-composer gate preserves expected word-wise
editing behavior

## Testing
- just fmt
- cargo test -p codex-tui

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Josh McKinney 2026-03-11 14:52:40 -07:00 committed by GitHub
parent 5a89660ae4
commit f548309797
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 49 additions and 5 deletions

View file

@ -3686,10 +3686,18 @@ impl App {
}
async fn handle_key_event(&mut self, tui: &mut tui::Tui, key_event: KeyEvent) {
// Some terminals, especially on macOS, encode Option+Left/Right as Option+b/f unless
// enhanced keyboard reporting is available. We only treat those word-motion fallbacks as
// agent-switch shortcuts when the composer is empty so we never steal the expected
// editing behavior for moving across words inside a draft.
let allow_agent_word_motion_fallback = !self.enhanced_keys_supported
&& self.chat_widget.composer_text_with_pending().is_empty();
if self.overlay.is_none()
&& self.chat_widget.no_modal_or_popup_active()
// Alt+Left/Right are also natural word-motion keys in the composer. Keep agent
// fast-switch available only once the draft is empty so editing behavior wins whenever
// there is text on screen.
&& self.chat_widget.composer_text_with_pending().is_empty()
&& previous_agent_shortcut_matches(key_event, allow_agent_word_motion_fallback)
{
if let Some(thread_id) = self.agent_navigation.adjacent_thread_id(
@ -3702,6 +3710,9 @@ impl App {
}
if self.overlay.is_none()
&& self.chat_widget.no_modal_or_popup_active()
// Mirror the previous-agent rule above: empty drafts may use these keys for thread
// switching, but non-empty drafts keep them for expected word-wise cursor motion.
&& self.chat_widget.composer_text_with_pending().is_empty()
&& next_agent_shortcut_matches(key_event, allow_agent_word_motion_fallback)
{
if let Some(thread_id) = self.agent_navigation.adjacent_thread_id(

View file

@ -121,8 +121,10 @@ fn previous_agent_word_motion_fallback(
key_event: KeyEvent,
allow_word_motion_fallback: bool,
) -> bool {
// macOS terminals often send Option+b/f as word-motion keys instead of Option+arrow events
// unless enhanced keyboard reporting is enabled.
// Some terminals, especially on macOS, send Option+b/f as word-motion keys instead of
// Option+arrow events unless enhanced keyboard reporting is enabled. Callers should only
// enable this fallback when the composer is empty so draft editing retains the expected
// word-wise motion behavior.
allow_word_motion_fallback
&& matches!(
key_event,
@ -145,8 +147,10 @@ fn previous_agent_word_motion_fallback(
#[cfg(target_os = "macos")]
fn next_agent_word_motion_fallback(key_event: KeyEvent, allow_word_motion_fallback: bool) -> bool {
// macOS terminals often send Option+b/f as word-motion keys instead of Option+arrow events
// unless enhanced keyboard reporting is enabled.
// Some terminals, especially on macOS, send Option+b/f as word-motion keys instead of
// Option+arrow events unless enhanced keyboard reporting is enabled. Callers should only
// enable this fallback when the composer is empty so draft editing retains the expected
// word-wise motion behavior.
allow_word_motion_fallback
&& matches!(
key_event,
@ -671,7 +675,15 @@ mod tests {
#[cfg(target_os = "macos")]
#[test]
fn agent_shortcut_matches_option_arrow_word_motion_fallbacks() {
fn agent_shortcut_matches_option_arrow_word_motion_fallbacks_only_when_allowed() {
assert!(previous_agent_shortcut_matches(
KeyEvent::new(KeyCode::Left, KeyModifiers::ALT),
false,
));
assert!(next_agent_shortcut_matches(
KeyEvent::new(KeyCode::Right, KeyModifiers::ALT),
false,
));
assert!(previous_agent_shortcut_matches(
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::ALT),
true,
@ -690,6 +702,27 @@ mod tests {
));
}
#[cfg(not(target_os = "macos"))]
#[test]
fn agent_shortcut_matches_option_arrows_only() {
assert!(previous_agent_shortcut_matches(
KeyEvent::new(KeyCode::Left, crossterm::event::KeyModifiers::ALT,),
false
));
assert!(next_agent_shortcut_matches(
KeyEvent::new(KeyCode::Right, crossterm::event::KeyModifiers::ALT,),
false
));
assert!(!previous_agent_shortcut_matches(
KeyEvent::new(KeyCode::Char('b'), crossterm::event::KeyModifiers::ALT,),
false
));
assert!(!next_agent_shortcut_matches(
KeyEvent::new(KeyCode::Char('f'), crossterm::event::KeyModifiers::ALT,),
false
));
}
#[test]
fn title_styles_nickname_and_role() {
let sender_thread_id = ThreadId::from_string("00000000-0000-0000-0000-000000000001")