mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(tui): shortcuts to change reasoning level temporarily (#18866)
## Summary Adds main-chat shortcuts for changing reasoning effort one step at a time: - `Alt+,` lowers reasoning (has the `<` arrow on the key) - `Alt+.` raises reasoning (similarly, has the `>` arrow) The shortcut updates the active session only. It does not persist the selected reasoning level as the default for future sessions. In Plan mode, it applies temporarily to Plan mode without opening the global-vs-Plan scope prompt. ## Details The shortcut uses the active model preset to decide which reasoning levels are valid. If the current session has no explicit reasoning effort, it starts from the model default. Each keypress moves to the next supported level in the requested direction. The shortcut only runs from the main chat surface. If a popup or modal is open, input remains owned by that UI. In Plan mode, the shortcut updates the in-memory Plan reasoning override directly. The model/reasoning picker still keeps the existing scope prompt for explicit picker changes. ## Notes Ctrl-plus and Ctrl-minus were considered, but terminals do not deliver those combinations consistently, so this PR uses Alt shortcuts instead. If the current effort is unsupported by the selected model, the shortcut skips to the nearest supported level in the requested direction. If there is no valid step, it shows the existing boundary message. ## Tests - `cargo test -p codex-tui reasoning_shortcuts` - `cargo test -p codex-tui reasoning_effort` - `cargo test -p codex-tui reasoning_shortcut` - `cargo test -p codex-tui footer_snapshots` - `cargo test -p codex-tui` - `just fix -p codex-tui` - `./tools/argument-comment-lint/run.py -p codex-tui -- --tests` --------- Co-authored-by: Eric Traut <etraut@openai.com>
This commit is contained in:
co-authored by
Eric Traut
parent
ffa6944587
commit
e502f0b52d
@@ -259,6 +259,53 @@ async fn reasoning_selection_in_plan_mode_matching_plan_effort_but_different_glo
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reasoning_shortcut_in_plan_mode_updates_plan_override_without_prompt_or_persist() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
|
||||
let plan_mask = collaboration_modes::plan_mask(chat.model_catalog.as_ref())
|
||||
.expect("expected plan collaboration mode");
|
||||
chat.set_collaboration_mask(plan_mask);
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
chat.set_reasoning_effort(Some(ReasoningEffortConfig::High));
|
||||
|
||||
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::UpdatePlanModeReasoningEffort(Some(ReasoningEffortConfig::High))
|
||||
)),
|
||||
"expected plan reasoning override update event; events: {events:?}"
|
||||
);
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.all(|event| !matches!(event, AppEvent::OpenPlanReasoningScopePrompt { .. })),
|
||||
"expected no Plan reasoning scope prompt event; events: {events:?}"
|
||||
);
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.all(|event| !matches!(event, AppEvent::PersistPlanModeReasoningEffort(_))),
|
||||
"expected no Plan reasoning persistence event; events: {events:?}"
|
||||
);
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.all(|event| !matches!(event, AppEvent::PersistModelSelection { .. })),
|
||||
"expected no global model persistence event; events: {events:?}"
|
||||
);
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.all(|event| !matches!(event, AppEvent::UpdateReasoningEffort(_))),
|
||||
"expected no global reasoning update event; events: {events:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_mode_reasoning_override_is_marked_current_in_reasoning_popup() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
|
||||
@@ -2077,6 +2077,105 @@ 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:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[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));
|
||||
|
||||
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_shortcut_clears_armed_quit_shortcut() {
|
||||
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.arm_quit_shortcut(key_hint::ctrl(KeyCode::Char('c')));
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('.'), KeyModifiers::ALT));
|
||||
|
||||
assert!(!chat.bottom_pane.quit_shortcut_hint_visible());
|
||||
assert!(chat.quit_shortcut_expires_at.is_none());
|
||||
assert!(chat.quit_shortcut_key.is_none());
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.all(|event| !matches!(event, AppEvent::Exit(_))),
|
||||
"did not expect reasoning shortcut to quit; events: {events:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reasoning_shortcut_is_ignored_with_model_popup_open() {
|
||||
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.open_model_popup();
|
||||
|
||||
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()
|
||||
.all(|event| !matches!(event, AppEvent::UpdateReasoningEffort(_))),
|
||||
"did not expect reasoning update while popup is active; events: {events:?}"
|
||||
);
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.all(|event| !matches!(event, AppEvent::PersistModelSelection { .. })),
|
||||
"did not expect model persistence while popup is active; events: {events:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reasoning_popup_shows_extra_high_with_space() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
|
||||
Reference in New Issue
Block a user