Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 57 additions & 48 deletions codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2435,58 +2435,67 @@ async fn model_reasoning_selection_popup_extra_high_warning_snapshot() {
assert_chatwidget_snapshot!("model_reasoning_selection_popup_extra_high_warning", popup);
}

#[tokio::test]
async fn alt_period_raises_reasoning_effort() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
chat.thread_id = Some(ThreadId::new());
chat.set_reasoning_effort(Some(ReasoningEffortConfig::Medium));

chat.handle_key_event(KeyEvent::new(KeyCode::Char('.'), KeyModifiers::ALT));

let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
assert!(
events
.iter()
.any(|event| matches!(event, AppEvent::UpdateModel(model) if model == "gpt-5.4")),
"expected model update event; events: {events:?}"
);
assert!(
events.iter().any(|event| matches!(
event,
AppEvent::UpdateReasoningEffort(Some(ReasoningEffortConfig::High))
)),
"expected reasoning update event; events: {events:?}"
);
assert!(
events
.iter()
.all(|event| !matches!(event, AppEvent::PersistModelSelection { .. })),
"expected no model persistence event; events: {events:?}"
);
async fn assert_reasoning_shortcuts_update_effort(
key_events: [KeyEvent; 2],
expected_effort: ReasoningEffortConfig,
expect_model_update: bool,
) {
for key_event in key_events {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
chat.thread_id = Some(ThreadId::new());
chat.set_reasoning_effort(Some(ReasoningEffortConfig::Medium));

chat.handle_key_event(key_event);

let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
if expect_model_update {
assert!(
events.iter().any(
|event| matches!(event, AppEvent::UpdateModel(model) if model == "gpt-5.4")
),
"expected model update event for {key_event:?}; events: {events:?}"
);
}
assert!(
events.iter().any(|event| matches!(
event,
AppEvent::UpdateReasoningEffort(Some(effort)) if effort == &expected_effort
)),
"expected reasoning update event for {key_event:?}; events: {events:?}"
);
assert!(
events
.iter()
.all(|event| !matches!(event, AppEvent::PersistModelSelection { .. })),
"expected no model persistence event for {key_event:?}; events: {events:?}"
);
}
}

#[tokio::test]
async fn alt_comma_lowers_reasoning_effort() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
chat.thread_id = Some(ThreadId::new());
chat.set_reasoning_effort(Some(ReasoningEffortConfig::Medium));

chat.handle_key_event(KeyEvent::new(KeyCode::Char(','), KeyModifiers::ALT));
async fn reasoning_up_shortcuts_raise_reasoning_effort() {
assert_reasoning_shortcuts_update_effort(
[
KeyEvent::new(KeyCode::Char('.'), KeyModifiers::ALT),
KeyEvent::new(KeyCode::Up, KeyModifiers::SHIFT),
],
ReasoningEffortConfig::High,
/*expect_model_update*/ true,
)
.await;
}

let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
assert!(
events.iter().any(|event| matches!(
event,
AppEvent::UpdateReasoningEffort(Some(ReasoningEffortConfig::Low))
)),
"expected reasoning update event; events: {events:?}"
);
assert!(
events
.iter()
.all(|event| !matches!(event, AppEvent::PersistModelSelection { .. })),
"expected no model persistence event; events: {events:?}"
);
#[tokio::test]
async fn reasoning_down_shortcuts_lower_reasoning_effort() {
assert_reasoning_shortcuts_update_effort(
[
KeyEvent::new(KeyCode::Char(','), KeyModifiers::ALT),
KeyEvent::new(KeyCode::Down, KeyModifiers::SHIFT),
],
ReasoningEffortConfig::Low,
/*expect_model_update*/ false,
)
.await;
}

#[tokio::test]
Expand Down
117 changes: 112 additions & 5 deletions codex-rs/tui/src/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use codex_config::types::MAX_FUNCTION_KEY;
use codex_config::types::TuiKeymap;
use crossterm::event::KeyCode;
use crossterm::event::KeyModifiers;
use serde::Serialize;
use std::collections::HashMap;

/// Runtime keymap used by TUI input handlers.
Expand Down Expand Up @@ -423,7 +424,7 @@ impl RuntimeKeymap {
)?,
};

let chat = ChatKeymap {
let mut chat = ChatKeymap {
interrupt_turn: resolve_bindings(
keymap.chat.interrupt_turn.as_ref(),
&defaults.chat.interrupt_turn,
Expand Down Expand Up @@ -738,6 +739,22 @@ impl RuntimeKeymap {
cancel: resolve_local!(keymap, defaults, vim_text_object, cancel),
};

// Reasoning arrow aliases are fallback defaults: existing explicit
// bindings on the same input path keep the keys, while explicit
// reasoning bindings remain authoritative.
if keymap.chat.decrease_reasoning_effort.is_none()
&& configured_main_surface_alias_is_used(keymap, "shift-down")
{
chat.decrease_reasoning_effort
.retain(|binding| *binding != key_hint::shift(KeyCode::Down));
}
if keymap.chat.increase_reasoning_effort.is_none()
&& configured_main_surface_alias_is_used(keymap, "shift-up")
{
chat.increase_reasoning_effort
.retain(|binding| *binding != key_hint::shift(KeyCode::Up));
}

let pager = PagerKeymap {
scroll_up: resolve_local!(keymap, defaults, pager, scroll_up),
scroll_down: resolve_local!(keymap, defaults, pager, scroll_down),
Expand Down Expand Up @@ -902,8 +919,14 @@ impl RuntimeKeymap {
},
chat: ChatKeymap {
interrupt_turn: default_bindings![plain(KeyCode::Esc)],
decrease_reasoning_effort: default_bindings![alt(KeyCode::Char(','))],
increase_reasoning_effort: default_bindings![alt(KeyCode::Char('.'))],
decrease_reasoning_effort: default_bindings![
alt(KeyCode::Char(',')),
shift(KeyCode::Down)
],
increase_reasoning_effort: default_bindings![
alt(KeyCode::Char('.')),
shift(KeyCode::Up)
],
edit_queued_message: default_bindings![alt(KeyCode::Up), shift(KeyCode::Left)],
},
composer: ComposerKeymap {
Expand Down Expand Up @@ -1856,6 +1879,52 @@ fn configured_bindings_to_preserve<const N: usize>(
configured_bindings
}

fn configured_main_surface_alias_is_used(keymap: &TuiKeymap, alias: &str) -> bool {
let mut global = keymap.global.clone();
if keymap.composer.submit.is_some() {
global.submit = None;
}
if keymap.composer.queue.is_some() {
global.queue = None;
}
if keymap.composer.toggle_shortcuts.is_some() {
global.toggle_shortcuts = None;
}

// Reasoning shortcuts run before composer/editor key handling, so fallback
// aliases must yield to any explicit binding on the same main-surface input
// path.
configured_context_alias_is_used(&global, alias)
|| configured_context_alias_is_used(&keymap.chat, alias)
|| configured_context_alias_is_used(&keymap.composer, alias)
|| configured_context_alias_is_used(&keymap.editor, alias)
|| configured_context_alias_is_used(&keymap.vim_normal, alias)
|| configured_context_alias_is_used(&keymap.vim_operator, alias)
|| configured_context_alias_is_used(&keymap.vim_text_object, alias)
}

fn configured_context_alias_is_used(context: &impl Serialize, alias: &str) -> bool {
let Ok(value) = serde_json::to_value(context) else {
return false;
};
keymap_value_contains_alias(&value, alias)
}

fn keymap_value_contains_alias(value: &serde_json::Value, alias: &str) -> bool {
match value {
serde_json::Value::String(value) => value == alias,
serde_json::Value::Array(values) => values
.iter()
.any(|value| keymap_value_contains_alias(value, alias)),
serde_json::Value::Object(values) => values
.values()
.any(|value| keymap_value_contains_alias(value, alias)),
serde_json::Value::Bool(_) | serde_json::Value::Number(_) | serde_json::Value::Null => {
false
}
}
}

fn resolve_new_default_bindings(
configured: Option<&KeybindingsSpec>,
fallback: &[KeyBinding],
Expand Down Expand Up @@ -2142,11 +2211,17 @@ mod tests {
);
assert_eq!(
runtime.chat.decrease_reasoning_effort,
vec![key_hint::alt(KeyCode::Char(','))]
vec![
key_hint::alt(KeyCode::Char(',')),
key_hint::shift(KeyCode::Down),
]
);
assert_eq!(
runtime.chat.increase_reasoning_effort,
vec![key_hint::alt(KeyCode::Char('.'))]
vec![
key_hint::alt(KeyCode::Char('.')),
key_hint::shift(KeyCode::Up),
]
);
assert_eq!(
runtime.chat.edit_queued_message,
Expand Down Expand Up @@ -2220,6 +2295,38 @@ mod tests {
);
}

#[test]
fn configured_main_surface_bindings_prune_reasoning_fallback_aliases() {
let mut keymap = TuiKeymap::default();
keymap.editor.move_up = Some(one("shift-up"));
keymap.vim_text_object.word = Some(one("shift-down"));

let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");

assert_eq!(runtime.editor.move_up, vec![key_hint::shift(KeyCode::Up)]);
assert_eq!(
runtime.vim_text_object.word,
vec![key_hint::shift(KeyCode::Down)]
);
assert_eq!(
runtime.chat.decrease_reasoning_effort,
vec![key_hint::alt(KeyCode::Char(','))]
);
assert_eq!(
runtime.chat.increase_reasoning_effort,
vec![key_hint::alt(KeyCode::Char('.'))]
);
}

#[test]
fn explicit_reasoning_binding_still_conflicts_with_editor_binding() {
let mut keymap = TuiKeymap::default();
keymap.editor.move_up = Some(one("shift-up"));
keymap.chat.increase_reasoning_effort = Some(one("shift-up"));

expect_conflict(&keymap, "chat.increase_reasoning_effort", "editor.move_up");
}

#[test]
fn configured_legacy_list_bindings_prune_new_default_overlaps() {
let mut keymap = TuiKeymap::default();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ Clear Terminal | ctrl-l | Global clear_terminal Clear Terminal Clear the termina
Toggle Vim Mode | unbound | Global toggle_vim_mode Toggle Vim Mode Turn Vim composer mode on or off. unbound Default
Toggle Raw Output | alt-r | Global toggle_raw_output Toggle Raw Output Toggle raw scrollback mode. alt-r Default
Interrupt Turn | esc | Chat interrupt_turn Interrupt Turn Interrupt the active turn. esc Default
Decrease Reasoning Effort | alt-, | Chat decrease_reasoning_effort Decrease Reasoning Effort Decrease reasoning effort. alt-, Default
Increase Reasoning Effort | alt-. | Chat increase_reasoning_effort Increase Reasoning Effort Increase reasoning effort. alt-. Default
Decrease Reasoning Effort | alt-,, shift-down | Chat decrease_reasoning_effort Decrease Reasoning Effort Decrease reasoning effort. alt-,, shift-down Default
Increase Reasoning Effort | alt-., shift-up | Chat increase_reasoning_effort Increase Reasoning Effort Increase reasoning effort. alt-., shift-up Default
Edit Queued Message | alt-up, shift-left | Chat edit_queued_message Edit Queued Message Edit the most recently queued message. alt-up, shift-left Default
Submit | enter | Composer submit Submit Submit the current composer draft. enter Default
Queue | tab | Composer queue Queue Queue the draft while a task is running. tab Default
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ expression: "render_picker(params, 120)"
Global - Toggle Vim Mode unbound
Global Toggle Raw Output alt-r
Chat Interrupt Turn esc
Chat Decrease Reasoning Effort alt-,
Chat Decrease Reasoning Effort alt-,, shift-down

left/right group · enter edit shortcut · * custom · - unbound · esc close
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ Clear Terminal | ctrl-l | Global clear_terminal Clear Terminal Clear the termina
Toggle Vim Mode | unbound | Global toggle_vim_mode Toggle Vim Mode Turn Vim composer mode on or off. unbound Default
Toggle Raw Output | alt-r | Global toggle_raw_output Toggle Raw Output Toggle raw scrollback mode. alt-r Default
Interrupt Turn | esc | Chat interrupt_turn Interrupt Turn Interrupt the active turn. esc Default
Decrease Reasoning Effort | alt-, | Chat decrease_reasoning_effort Decrease Reasoning Effort Decrease reasoning effort. alt-, Default
Increase Reasoning Effort | alt-. | Chat increase_reasoning_effort Increase Reasoning Effort Increase reasoning effort. alt-. Default
Decrease Reasoning Effort | alt-,, shift-down | Chat decrease_reasoning_effort Decrease Reasoning Effort Decrease reasoning effort. alt-,, shift-down Default
Increase Reasoning Effort | alt-., shift-up | Chat increase_reasoning_effort Increase Reasoning Effort Increase reasoning effort. alt-., shift-up Default
Edit Queued Message | alt-up, shift-left | Chat edit_queued_message Edit Queued Message Edit the most recently queued message. alt-up, shift-left Default
Submit | enter | Composer submit Submit Submit the current composer draft. enter Default
Queue | tab | Composer queue Queue Queue the draft while a task is running. tab Default
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ expression: "render_picker(params, 78)"
Global - Toggle Vim Mode unbound
Global Toggle Raw Output alt-r
Chat Interrupt Turn esc
Chat Decrease Reasoning Effort alt-,
Chat Decrease Reasoning Effort alt-,, shift-down

left/right group · enter edit shortcut · * custom · - unbound · esc close
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ expression: "render_picker(params, 120)"
Global - Toggle Vim Mode unbound
Global Toggle Raw Output alt-r
Chat Interrupt Turn esc
Chat Decrease Reasoning Effort alt-,
Chat Decrease Reasoning Effort alt-,, shift-down

left/right group · enter edit shortcut · * custom · - unbound · esc close
Loading