feat(tui): make reasoning shortcuts configurable

This commit is contained in:
Felipe Coury
2026-04-23 10:35:35 -03:00
parent 4b478b593f
commit 2ddac81e43
20 changed files with 327 additions and 109 deletions
+11 -1
View File
@@ -29,7 +29,7 @@ use std::collections::BTreeMap;
pub enum TuiKeymapPreset {
/// Pointer alias to the latest shipped preset.
///
/// Today this resolves to `v3`.
/// Today this resolves to `v4`.
#[default]
Latest,
/// Frozen initial configurable keymap defaults.
@@ -38,6 +38,8 @@ pub enum TuiKeymapPreset {
V2,
/// Current defaults with the copy shortcut exposed through keymap config.
V3,
/// Current defaults with reasoning shortcuts exposed through keymap config.
V4,
}
/// Normalized string representation of a keybinding (for example `ctrl-a`).
@@ -116,6 +118,10 @@ pub struct TuiGlobalKeymap {
pub toggle_shortcuts: Option<KeybindingsSpec>,
/// Toggle Vim mode for the composer input.
pub toggle_vim_mode: Option<KeybindingsSpec>,
/// Lower the active reasoning level when possible.
pub reasoning_down: Option<KeybindingsSpec>,
/// Raise the active reasoning level when possible.
pub reasoning_up: Option<KeybindingsSpec>,
}
/// Chat context keybindings. These override corresponding `global` actions.
@@ -126,6 +132,10 @@ pub struct TuiChatKeymap {
pub edit_previous_message: Option<KeybindingsSpec>,
/// Confirm editing the selected previous message.
pub confirm_edit_previous_message: Option<KeybindingsSpec>,
/// Lower the active reasoning level when possible.
pub reasoning_down: Option<KeybindingsSpec>,
/// Raise the active reasoning level when possible.
pub reasoning_up: Option<KeybindingsSpec>,
}
/// Composer context keybindings. These override corresponding `global` actions.
+50 -3
View File
@@ -2204,7 +2204,9 @@
},
"chat": {
"confirm_edit_previous_message": null,
"edit_previous_message": null
"edit_previous_message": null,
"reasoning_down": null,
"reasoning_up": null
},
"composer": {
"queue": null,
@@ -2236,6 +2238,8 @@
"open_external_editor": null,
"open_transcript": null,
"queue": null,
"reasoning_down": null,
"reasoning_up": null,
"submit": null,
"toggle_shortcuts": null,
"toggle_vim_mode": null
@@ -2458,6 +2462,22 @@
}
],
"description": "In an empty composer, begin or advance \"edit previous message\" flow."
},
"reasoning_down": {
"allOf": [
{
"$ref": "#/definitions/KeybindingsSpec"
}
],
"description": "Lower the active reasoning level when possible."
},
"reasoning_up": {
"allOf": [
{
"$ref": "#/definitions/KeybindingsSpec"
}
],
"description": "Raise the active reasoning level when possible."
}
},
"type": "object"
@@ -2680,6 +2700,22 @@
],
"description": "Queue the current composer draft while a task is running."
},
"reasoning_down": {
"allOf": [
{
"$ref": "#/definitions/KeybindingsSpec"
}
],
"description": "Lower the active reasoning level when possible."
},
"reasoning_up": {
"allOf": [
{
"$ref": "#/definitions/KeybindingsSpec"
}
],
"description": "Raise the active reasoning level when possible."
},
"submit": {
"allOf": [
{
@@ -2734,7 +2770,9 @@
],
"default": {
"confirm_edit_previous_message": null,
"edit_previous_message": null
"edit_previous_message": null,
"reasoning_down": null,
"reasoning_up": null
}
},
"composer": {
@@ -2787,6 +2825,8 @@
"open_external_editor": null,
"open_transcript": null,
"queue": null,
"reasoning_down": null,
"reasoning_up": null,
"submit": null,
"toggle_shortcuts": null,
"toggle_vim_mode": null
@@ -2912,7 +2952,7 @@
"description": "Versioned keymap defaults.",
"oneOf": [
{
"description": "Pointer alias to the latest shipped preset.\n\nToday this resolves to `v3`.",
"description": "Pointer alias to the latest shipped preset.\n\nToday this resolves to `v4`.",
"enum": [
"latest"
],
@@ -2938,6 +2978,13 @@
"v3"
],
"type": "string"
},
{
"description": "Current defaults with reasoning shortcuts exposed through keymap config.",
"enum": [
"v4"
],
"type": "string"
}
]
},
@@ -397,6 +397,8 @@ pub(crate) struct ChatComposer {
footer_external_editor_key: Option<KeyBinding>,
footer_edit_previous_key: Option<KeyBinding>,
footer_show_transcript_key: Option<KeyBinding>,
footer_reasoning_down_key: Option<KeyBinding>,
footer_reasoning_up_key: Option<KeyBinding>,
}
#[derive(Clone, Debug)]
@@ -554,6 +556,8 @@ impl ChatComposer {
footer_external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))),
footer_edit_previous_key: Some(key_hint::plain(KeyCode::Esc)),
footer_show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))),
footer_reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
footer_reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
};
// Apply configuration via the setter to keep side-effects centralized.
this.set_disable_paste_burst(disable_paste_burst);
@@ -636,6 +640,8 @@ impl ChatComposer {
self.footer_external_editor_key = primary_binding(&keymap.app.open_external_editor);
self.footer_edit_previous_key = primary_binding(&keymap.chat.edit_previous_message);
self.footer_show_transcript_key = primary_binding(&keymap.app.open_transcript);
self.footer_reasoning_down_key = primary_binding(&keymap.chat.reasoning_down);
self.footer_reasoning_up_key = primary_binding(&keymap.chat.reasoning_up);
}
pub fn set_collaboration_mode_indicator(
@@ -3318,6 +3324,8 @@ impl ChatComposer {
is_wsl,
status_line_value: self.status_line_value.clone(),
status_line_enabled: self.status_line_enabled,
reasoning_down_key: self.footer_reasoning_down_key,
reasoning_up_key: self.footer_reasoning_up_key,
active_agent_label: self.active_agent_label.clone(),
}
}
+58 -22
View File
@@ -76,6 +76,8 @@ pub(crate) struct FooterProps {
pub(crate) quit_shortcut_key: KeyBinding,
pub(crate) status_line_value: Option<Line<'static>>,
pub(crate) status_line_enabled: bool,
pub(crate) reasoning_down_key: Option<KeyBinding>,
pub(crate) reasoning_up_key: Option<KeyBinding>,
/// Active thread label shown when the footer is rendering contextual information instead of an
/// instructional hint.
///
@@ -622,6 +624,8 @@ fn footer_from_props_lines(
esc_backtrack_hint: props.esc_backtrack_hint,
is_wsl: props.is_wsl,
collaboration_modes_enabled: props.collaboration_modes_enabled,
reasoning_down_key: props.reasoning_down_key,
reasoning_up_key: props.reasoning_up_key,
};
shortcut_overlay_lines(state)
}
@@ -739,6 +743,8 @@ struct ShortcutsState {
esc_backtrack_hint: bool,
is_wsl: bool,
collaboration_modes_enabled: bool,
reasoning_down_key: Option<KeyBinding>,
reasoning_up_key: Option<KeyBinding>,
}
fn quit_shortcut_reminder_line(key: KeyBinding) -> Line<'static> {
@@ -791,11 +797,15 @@ fn shortcut_overlay_lines(state: ShortcutsState) -> Vec<Line<'static>> {
ShortcutId::Quit => quit = text,
ShortcutId::ShowTranscript => show_transcript = text,
ShortcutId::ChangeMode => change_mode = text,
ShortcutId::ReasoningDown => reasoning_down = text,
ShortcutId::ReasoningUp => reasoning_up = text,
}
}
}
if let Some(key) = state.reasoning_down_key {
reasoning_down = Line::from(vec![key.into(), " reasoning down".into()]);
}
if let Some(key) = state.reasoning_up_key {
reasoning_up = Line::from(vec![key.into(), " reasoning up".into()]);
}
let mut ordered = vec![
commands,
@@ -899,8 +909,6 @@ enum ShortcutId {
Quit,
ShowTranscript,
ChangeMode,
ReasoningDown,
ReasoningUp,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -1092,24 +1100,6 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
prefix: "",
label: " to change mode",
},
ShortcutDescriptor {
id: ShortcutId::ReasoningDown,
bindings: &[ShortcutBinding {
key: key_hint::alt(KeyCode::Char(',')),
condition: DisplayCondition::Always,
}],
prefix: "",
label: " reasoning down",
},
ShortcutDescriptor {
id: ShortcutId::ReasoningUp,
bindings: &[ShortcutBinding {
key: key_hint::alt(KeyCode::Char('.')),
condition: DisplayCondition::Always,
}],
prefix: "",
label: " reasoning up",
},
];
#[cfg(test)]
@@ -1376,6 +1366,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
},
);
@@ -1392,6 +1384,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
},
);
@@ -1408,6 +1402,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
},
);
@@ -1424,6 +1420,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
},
);
@@ -1440,6 +1438,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
},
);
@@ -1456,6 +1456,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
},
);
@@ -1472,6 +1474,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
},
);
@@ -1488,6 +1492,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
},
Some(72),
@@ -1506,6 +1512,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
},
/*percent*/ None,
@@ -1524,6 +1532,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
},
);
@@ -1538,6 +1548,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
};
@@ -1565,6 +1577,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
};
@@ -1585,6 +1599,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: Some(Line::from("Status line content".to_string())),
status_line_enabled: true,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
};
@@ -1600,6 +1616,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: Some(Line::from("Status line content".to_string())),
status_line_enabled: true,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
};
@@ -1615,6 +1633,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: Some(Line::from("Status line content".to_string())),
status_line_enabled: true,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
};
@@ -1630,6 +1650,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None, // command timed out / empty
status_line_enabled: true,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
};
@@ -1651,6 +1673,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
};
@@ -1672,6 +1696,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: true,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
};
@@ -1696,6 +1722,8 @@ mod tests {
"Status line content that should truncate before the mode indicator".to_string(),
)),
status_line_enabled: true,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
};
@@ -1717,6 +1745,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: Some("Robie [explorer]".to_string()),
};
@@ -1732,6 +1762,8 @@ mod tests {
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: Some(Line::from("Status line content".to_string())),
status_line_enabled: true,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: Some("Robie [explorer]".to_string()),
};
@@ -1753,6 +1785,8 @@ mod tests {
.to_string(),
)),
status_line_enabled: true,
reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))),
reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))),
active_agent_label: None,
};
@@ -1807,6 +1841,8 @@ mod tests {
esc_backtrack_hint: false,
is_wsl,
collaboration_modes_enabled: false,
reasoning_down_key: None,
reasoning_up_key: None,
})
.expect("shortcut binding")
.key;
+8
View File
@@ -355,6 +355,7 @@ use crate::history_cell::WebSearchCell;
use crate::key_hint;
use crate::key_hint::KeyBinding;
use crate::key_hint::KeyBindingListExt;
use crate::keymap::ChatKeymap;
use crate::keymap::RuntimeKeymap;
use crate::keymap_setup;
#[cfg(test)]
@@ -824,6 +825,7 @@ pub(crate) struct ChatWidget {
/// Holds the platform clipboard lease so copied text remains available while supported.
clipboard_lease: Option<crate::clipboard_copy::ClipboardLease>,
copy_last_response_binding: Vec<KeyBinding>,
chat_keymap: ChatKeymap,
/// Raw markdown of the most recently completed agent response that
/// survived any local thread rollback.
last_agent_markdown: Option<String>,
@@ -5162,6 +5164,10 @@ impl ChatWidget {
.as_ref()
.map(|keymap| keymap.app.copy.clone())
.unwrap_or_else(|| RuntimeKeymap::defaults().app.copy);
let chat_keymap = runtime_keymap
.as_ref()
.map(|keymap| keymap.chat.clone())
.unwrap_or_else(|| RuntimeKeymap::defaults().chat);
let mut widget = Self {
app_event_tx: app_event_tx.clone(),
@@ -5205,6 +5211,7 @@ impl ChatWidget {
plan_stream_controller: None,
clipboard_lease: None,
copy_last_response_binding,
chat_keymap,
running_commands: HashMap::new(),
collab_agent_metadata: HashMap::new(),
pending_collab_spawn_requests: HashMap::new(),
@@ -7845,6 +7852,7 @@ impl ChatWidget {
) {
self.config.tui_keymap = keymap_config;
self.copy_last_response_binding = runtime_keymap.app.copy.clone();
self.chat_keymap = runtime_keymap.chat.clone();
self.bottom_pane.set_keymap_bindings(runtime_keymap);
self.request_redraw();
}
@@ -15,14 +15,14 @@
use codex_protocol::config_types::ModeKind;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use crossterm::event::KeyModifiers;
use strum::IntoEnumIterator;
use super::ChatWidget;
use crate::app_event::AppEvent;
use crate::key_hint::KeyBindingListExt;
use crate::keymap::ChatKeymap;
/// Direction requested by a reasoning-level shortcut.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -32,15 +32,17 @@ pub(super) enum ReasoningShortcutDirection {
}
impl ReasoningShortcutDirection {
fn from_key_event(key_event: KeyEvent) -> Option<Self> {
if key_event.kind != KeyEventKind::Press || key_event.modifiers != KeyModifiers::ALT {
fn from_key_event(key_event: KeyEvent, keymap: &ChatKeymap) -> Option<Self> {
if key_event.kind != KeyEventKind::Press {
return None;
}
match key_event.code {
KeyCode::Char(',') => Some(Self::Lower),
KeyCode::Char('.') => Some(Self::Raise),
_ => None,
if keymap.reasoning_down.is_pressed(key_event) {
Some(Self::Lower)
} else if keymap.reasoning_up.is_pressed(key_event) {
Some(Self::Raise)
} else {
None
}
}
@@ -66,7 +68,9 @@ impl ChatWidget {
/// persisting them. In Plan mode, shortcuts apply only to the active
/// Plan-mode override and skip the global-vs-Plan scope prompt.
pub(super) fn handle_reasoning_shortcut(&mut self, key_event: KeyEvent) -> bool {
let Some(direction) = ReasoningShortcutDirection::from_key_event(key_event) else {
let Some(direction) =
ReasoningShortcutDirection::from_key_event(key_event, &self.chat_keymap)
else {
return false;
};
@@ -176,8 +180,43 @@ fn effort_rank(effort: ReasoningEffortConfig) -> i32 {
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
use pretty_assertions::assert_eq;
#[test]
fn reasoning_shortcut_direction_uses_configured_chat_bindings() {
let keymap = ChatKeymap {
edit_previous_message: Vec::new(),
confirm_edit_previous_message: Vec::new(),
reasoning_down: vec![crate::key_hint::ctrl(KeyCode::Char(','))],
reasoning_up: vec![crate::key_hint::ctrl(KeyCode::Char('.'))],
};
assert_eq!(
ReasoningShortcutDirection::from_key_event(
KeyEvent::new(KeyCode::Char(','), KeyModifiers::CONTROL),
&keymap,
),
Some(ReasoningShortcutDirection::Lower)
);
assert_eq!(
ReasoningShortcutDirection::from_key_event(
KeyEvent::new(KeyCode::Char('.'), KeyModifiers::CONTROL),
&keymap,
),
Some(ReasoningShortcutDirection::Raise)
);
assert_eq!(
ReasoningShortcutDirection::from_key_event(
KeyEvent::new(KeyCode::Char(','), KeyModifiers::ALT),
&keymap,
),
None
);
}
#[test]
fn next_reasoning_effort_raises_from_default_anchor() {
let choices = vec![
@@ -212,6 +212,7 @@ pub(super) async fn make_chatwidget_manual(
plan_stream_controller: None,
clipboard_lease: None,
copy_last_response_binding: RuntimeKeymap::defaults().app.copy,
chat_keymap: RuntimeKeymap::defaults().chat,
pending_guardian_review_status: PendingGuardianReviewStatus::default(),
terminal_title_status_kind: TerminalTitleStatusKind::Working,
last_agent_markdown: None,
+49 -2
View File
@@ -33,7 +33,7 @@ use std::collections::HashMap;
///
/// 1. Context-specific binding (`tui.keymap.<context>`).
/// 2. `tui.keymap.global` for actions that support global fallback.
/// 3. Built-in preset defaults (`latest` currently points to `v3`).
/// 3. Built-in preset defaults (`latest` currently points to `v4`).
#[derive(Clone, Debug)]
pub(crate) struct RuntimeKeymap {
pub(crate) app: AppKeymap,
@@ -72,6 +72,10 @@ pub(crate) struct ChatKeymap {
pub(crate) edit_previous_message: Vec<KeyBinding>,
/// Confirm edit-previous selection.
pub(crate) confirm_edit_previous_message: Vec<KeyBinding>,
/// Lower the active reasoning level when possible.
pub(crate) reasoning_down: Vec<KeyBinding>,
/// Raise the active reasoning level when possible.
pub(crate) reasoning_up: Vec<KeyBinding>,
}
/// Composer-level keybindings validated in the second app-scope conflict pass.
@@ -376,6 +380,8 @@ impl RuntimeKeymap {
chat,
confirm_edit_previous_message
),
reasoning_down: resolve_with_global!(keymap, defaults, chat, reasoning_down),
reasoning_up: resolve_with_global!(keymap, defaults, chat, reasoning_up),
};
let composer = ComposerKeymap {
@@ -521,7 +527,8 @@ impl RuntimeKeymap {
fn defaults_for_preset(preset: TuiKeymapPreset) -> Self {
match preset {
TuiKeymapPreset::Latest | TuiKeymapPreset::V3 => Self::defaults_v3(),
TuiKeymapPreset::Latest | TuiKeymapPreset::V4 => Self::defaults_v4(),
TuiKeymapPreset::V3 => Self::defaults_v3(),
TuiKeymapPreset::V2 => Self::defaults_v2(),
TuiKeymapPreset::V1 => Self::defaults_v1(),
}
@@ -543,6 +550,8 @@ impl RuntimeKeymap {
chat: ChatKeymap {
edit_previous_message: default_bindings![plain(KeyCode::Esc)],
confirm_edit_previous_message: default_bindings![plain(KeyCode::Enter)],
reasoning_down: default_bindings![],
reasoning_up: default_bindings![],
},
composer: ComposerKeymap {
submit: default_bindings![plain(KeyCode::Enter)],
@@ -751,6 +760,17 @@ impl RuntimeKeymap {
defaults
}
/// Current keymap defaults for preset `v4`.
///
/// This keeps earlier presets intact while moving reasoning-step shortcuts
/// into configurable keymap defaults.
fn defaults_v4() -> Self {
let mut defaults = Self::defaults_v3();
defaults.chat.reasoning_down = default_bindings![alt(KeyCode::Char(','))];
defaults.chat.reasoning_up = default_bindings![alt(KeyCode::Char('.'))];
defaults
}
/// Reject ambiguous bindings in scopes that are evaluated together.
///
/// We validate in multiple passes because runtime handling has mixed
@@ -782,6 +802,8 @@ impl RuntimeKeymap {
"confirm_edit_previous_message",
self.chat.confirm_edit_previous_message.as_slice(),
),
("reasoning_down", self.chat.reasoning_down.as_slice()),
("reasoning_up", self.chat.reasoning_up.as_slice()),
],
)?;
@@ -1281,6 +1303,19 @@ mod tests {
assert_eq!(runtime.app.copy, vec![key_hint::ctrl(KeyCode::Char('o'))]);
}
#[test]
fn default_latest_reasoning_bindings_are_alt_comma_and_alt_period() {
let runtime = RuntimeKeymap::defaults();
assert_eq!(
runtime.chat.reasoning_down,
vec![key_hint::alt(KeyCode::Char(','))]
);
assert_eq!(
runtime.chat.reasoning_up,
vec![key_hint::alt(KeyCode::Char('.'))]
);
}
#[test]
fn invalid_global_copy_binding_reports_global_path() {
let mut keymap = TuiKeymap::default();
@@ -1303,6 +1338,18 @@ mod tests {
);
}
#[test]
fn resolves_reasoning_binding_from_global_fallback() {
let mut keymap = TuiKeymap::default();
keymap.global.reasoning_up = Some(one("ctrl-."));
let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");
assert_eq!(
runtime.chat.reasoning_up,
vec![key_hint::ctrl(KeyCode::Char('.'))]
);
}
#[test]
fn rejects_conflicting_editor_bindings() {
let mut keymap = TuiKeymap::default();
+3 -1
View File
@@ -773,7 +773,7 @@ mod tests {
line.push_str(symbol);
}
}
line
line.trim_end().to_string()
})
.collect::<Vec<_>>()
.join("\n")
@@ -880,6 +880,8 @@ mod tests {
"Global.open_external_editor",
"Global.copy",
"Global.toggle_vim_mode",
"Chat.reasoning_down",
"Chat.reasoning_up",
"Editor.delete_backward_word",
"Editor.delete_forward_word",
"Editor.move_word_left",
+6
View File
@@ -38,6 +38,8 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
action("global", "Global", "toggle_vim_mode", "Turn Vim composer mode on or off."),
action("chat", "Chat", "edit_previous_message", "Begin or advance edit-previous-message when the composer is empty."),
action("chat", "Chat", "confirm_edit_previous_message", "Confirm the selected previous message to edit."),
action("chat", "Chat", "reasoning_down", "Lower the active reasoning level when possible."),
action("chat", "Chat", "reasoning_up", "Raise the active reasoning level when possible."),
action("composer", "Composer", "submit", "Submit the current composer draft."),
action("composer", "Composer", "queue", "Queue the draft while a task is running."),
action("composer", "Composer", "toggle_shortcuts", "Show or hide the composer shortcut overlay."),
@@ -152,6 +154,8 @@ pub(super) fn binding_slot<'a>(
("global", "toggle_vim_mode") => Some(&mut keymap.global.toggle_vim_mode),
("chat", "edit_previous_message") => Some(&mut keymap.chat.edit_previous_message),
("chat", "confirm_edit_previous_message") => Some(&mut keymap.chat.confirm_edit_previous_message),
("chat", "reasoning_down") => Some(&mut keymap.chat.reasoning_down),
("chat", "reasoning_up") => Some(&mut keymap.chat.reasoning_up),
("composer", "submit") => Some(&mut keymap.composer.submit),
("composer", "queue") => Some(&mut keymap.composer.queue),
("composer", "toggle_shortcuts") => Some(&mut keymap.composer.toggle_shortcuts),
@@ -254,6 +258,8 @@ pub(super) fn bindings_for_action<'a>(
("global", "toggle_vim_mode") => Some(runtime_keymap.app.toggle_vim_mode.as_slice()),
("chat", "edit_previous_message") => Some(runtime_keymap.chat.edit_previous_message.as_slice()),
("chat", "confirm_edit_previous_message") => Some(runtime_keymap.chat.confirm_edit_previous_message.as_slice()),
("chat", "reasoning_down") => Some(runtime_keymap.chat.reasoning_down.as_slice()),
("chat", "reasoning_up") => Some(runtime_keymap.chat.reasoning_up.as_slice()),
("composer", "submit") => Some(runtime_keymap.composer.submit.as_slice()),
("composer", "queue") => Some(runtime_keymap.composer.queue.as_slice()),
("composer", "toggle_shortcuts") => Some(runtime_keymap.composer.toggle_shortcuts.as_slice()),
+2
View File
@@ -61,6 +61,8 @@ const KEYMAP_COMMON_ACTIONS: &[(&str, &str)] = &[
("global", "open_external_editor"),
("global", "copy"),
("global", "toggle_vim_mode"),
("chat", "reasoning_down"),
("chat", "reasoning_up"),
("editor", "delete_backward_word"),
("editor", "delete_forward_word"),
("editor", "move_word_left"),
@@ -8,9 +8,9 @@ Copy | ctrl-o | Global copy Copy Copy the last agent response to the clipboard.
Toggle Vim Mode | unbound | Global toggle_vim_mode Toggle Vim Mode Turn Vim composer mode on or off. unbound Default
Edit Previous Message | esc | Chat edit_previous_message Edit Previous Message Begin or advance edit-previous-message when the composer is empty. esc Default
Confirm Edit Previous Message | enter | Chat confirm_edit_previous_message Confirm Edit Previous Message Confirm the selected previous message to edit. enter Default
Reasoning Down | alt-, | Chat reasoning_down Reasoning Down Lower the active reasoning level when possible. alt-, Default
Reasoning Up | alt-. | Chat reasoning_up Reasoning Up Raise the active reasoning level when possible. alt-. 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
Toggle Shortcuts | ?, shift-? | Composer toggle_shortcuts Toggle Shortcuts Show or hide the composer shortcut overlay. ?, shift-? Default
Insert Newline | ctrl-j, ctrl-m, enter, shift-enter | Editor insert_newline Insert Newline Insert a newline in the editor. ctrl-j, ctrl-m, enter, shift-enter Default
Move Left | left, ctrl-b | Editor move_left Move Left Move the cursor left. left, ctrl-b Default
Move Right | right, ctrl-f | Editor move_right Move Right Move the cursor right. right, ctrl-f Default
@@ -2,22 +2,22 @@
source: tui/src/keymap_setup.rs
expression: "render_picker(params, 120)"
---
Keymap
All configurable shortcuts.
91 actions, 1 customized, 1 unbound.
[All] Common Customized (1) Unbound (1) App Composer Editor Vim Navigation Approval
Type to search shortcuts
› Global Open Transcript ctrl-t
Global Open External Editor ctrl-g
Global Copy ctrl-o
Global - Toggle Vim Mode unbound
Chat Edit Previous Message esc
Chat Confirm Edit Previous Message enter
Composer * Submit ctrl-enter
Composer Queue tab
Open Transcript: Open the transcript overlay.
Keymap
All configurable shortcuts.
93 actions, 1 customized, 1 unbound.
[All] Common Customized (1) Unbound (1) App Composer Editor Vim Navigation Approval
Type to search shortcuts
› Global Open Transcript ctrl-t
Global Open External Editor ctrl-g
Global Copy ctrl-o
Global - Toggle Vim Mode unbound
Chat Edit Previous Message esc
Chat Confirm Edit Previous Message enter
Chat Reasoning Down alt-,
Chat Reasoning Up alt-.
Open Transcript: Open the transcript overlay.
left/right group · enter edit shortcut · * custom · - unbound · esc close
@@ -2,11 +2,11 @@
source: tui/src/keymap_setup.rs
expression: snapshot
---
tab: All (91 selectable)
tab: Common (19 selectable)
tab: All (93 selectable)
tab: Common (21 selectable)
tab: Customized (0) (0 selectable)
tab: Unbound (1) (1 selectable)
tab: App (6 selectable)
tab: App (8 selectable)
tab: Composer (3 selectable)
tab: Editor (16 selectable)
tab: Vim (34 selectable)
@@ -18,9 +18,9 @@ Copy | ctrl-o | Global copy Copy Copy the last agent response to the clipboard.
Toggle Vim Mode | unbound | Global toggle_vim_mode Toggle Vim Mode Turn Vim composer mode on or off. unbound Default
Edit Previous Message | esc | Chat edit_previous_message Edit Previous Message Begin or advance edit-previous-message when the composer is empty. esc Default
Confirm Edit Previous Message | enter | Chat confirm_edit_previous_message Confirm Edit Previous Message Confirm the selected previous message to edit. enter Default
Reasoning Down | alt-, | Chat reasoning_down Reasoning Down Lower the active reasoning level when possible. alt-, Default
Reasoning Up | alt-. | Chat reasoning_up Reasoning Up Raise the active reasoning level when possible. alt-. 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
Toggle Shortcuts | ?, shift-? | Composer toggle_shortcuts Toggle Shortcuts Show or hide the composer shortcut overlay. ?, shift-? Default
Insert Newline | ctrl-j, ctrl-m, enter, shift-enter | Editor insert_newline Insert Newline Insert a newline in the editor. ctrl-j, ctrl-m, enter, shift-enter Default
Move Left | left, ctrl-b | Editor move_left Move Left Move the cursor left. left, ctrl-b Default
Move Right | right, ctrl-f | Editor move_right Move Right Move the cursor right. right, ctrl-f Default
@@ -2,23 +2,23 @@
source: tui/src/keymap_setup.rs
expression: "render_picker(params, 78)"
---
Keymap
All configurable shortcuts.
91 actions, 0 customized, 1 unbound.
[All] Common Customized (0) Unbound (1) App Composer Editor Vim
Navigation Approval
Type to search shortcuts
› Global Open Transcript ctrl-t
Global Open External Editor ctrl-g
Global Copy ctrl-o
Global - Toggle Vim Mode unbound
Chat Edit Previous Message esc
Chat Confirm Edit Previous Message enter
Composer Submit enter
Composer Queue tab
Open Transcript: Open the transcript overlay.
Keymap
All configurable shortcuts.
93 actions, 0 customized, 1 unbound.
[All] Common Customized (0) Unbound (1) App Composer Editor Vim
Navigation Approval
Type to search shortcuts
› Global Open Transcript ctrl-t
Global Open External Editor ctrl-g
Global Copy ctrl-o
Global - Toggle Vim Mode unbound
Chat Edit Previous Message esc
Chat Confirm Edit Previous Message enter
Chat Reasoning Down alt-,
Chat Reasoning Up alt-.
Open Transcript: Open the transcript overlay.
left/right group · enter edit shortcut · * custom · - unbound · esc close
@@ -2,22 +2,22 @@
source: tui/src/keymap_setup.rs
expression: "render_picker(params, 120)"
---
Keymap
All configurable shortcuts.
91 actions, 0 customized, 1 unbound.
[All] Common Customized (0) Unbound (1) App Composer Editor Vim Navigation Approval
Type to search shortcuts
› Global Open Transcript ctrl-t
Global Open External Editor ctrl-g
Global Copy ctrl-o
Global - Toggle Vim Mode unbound
Chat Edit Previous Message esc
Chat Confirm Edit Previous Message enter
Composer Submit enter
Composer Queue tab
Open Transcript: Open the transcript overlay.
Keymap
All configurable shortcuts.
93 actions, 0 customized, 1 unbound.
[All] Common Customized (0) Unbound (1) App Composer Editor Vim Navigation Approval
Type to search shortcuts
› Global Open Transcript ctrl-t
Global Open External Editor ctrl-g
Global Copy ctrl-o
Global - Toggle Vim Mode unbound
Chat Edit Previous Message esc
Chat Confirm Edit Previous Message enter
Chat Reasoning Down alt-,
Chat Reasoning Up alt-.
Open Transcript: Open the transcript overlay.
left/right group · enter edit shortcut · * custom · - unbound · esc close
+6 -4
View File
@@ -36,8 +36,9 @@ Precedence is applied in this order (highest first):
### Presets
- `latest`: moving alias for the newest preset; today `latest -> v3`
- `v3`: current defaults, exposing `copy = "ctrl-o"` in the keymap
- `latest`: moving alias for the newest preset; today `latest -> v4`
- `v4`: current defaults, exposing reasoning shortcuts (`alt-,` / `alt-.`) in the keymap
- `v3`: exposes `copy = "ctrl-o"` in the keymap
- `v2`: restores `alt-d` as a `delete_forward_word` alias
- `v1`: frozen initial configurable keymap defaults
@@ -50,8 +51,9 @@ TODO(docs): mirror this preset migration note on developers.openai.com.
- `global`: `open_transcript`, `open_external_editor`, `copy`, `edit_previous_message`,
`confirm_edit_previous_message`, `submit`, `queue`, `toggle_shortcuts`,
`toggle_vim_mode`
- `chat`: `edit_previous_message`, `confirm_edit_previous_message`
`toggle_vim_mode`, `reasoning_down`, `reasoning_up`
- `chat`: `edit_previous_message`, `confirm_edit_previous_message`,
`reasoning_down`, `reasoning_up`
- `composer`: `submit`, `queue`, `toggle_shortcuts`
- `editor`: `insert_newline`, `move_left`, `move_right`, `move_up`, `move_down`,
`move_word_left`, `move_word_right`, `move_line_start`, `move_line_end`,
+7 -1
View File
@@ -18,7 +18,7 @@
[tui.keymap]
# Preset alias for built-in defaults.
# "latest" currently points to "v3".
# "latest" currently points to "v4".
preset = "latest"
[tui.keymap.global]
@@ -42,11 +42,17 @@ toggle_shortcuts = ["?", "shift-?"]
# Toggle Vim mode for the composer input.
# Empty list keeps this unbound by default.
toggle_vim_mode = []
# Lower active reasoning level when possible.
reasoning_down = "alt-,"
# Raise active reasoning level when possible.
reasoning_up = "alt-."
[tui.keymap.chat]
# Overrides [tui.keymap.global] for chat-only behavior.
edit_previous_message = "esc"
confirm_edit_previous_message = "enter"
reasoning_down = "alt-,"
reasoning_up = "alt-."
[tui.keymap.composer]
# Overrides [tui.keymap.global] for composer behavior.
+6 -2
View File
@@ -7,14 +7,14 @@ For runtime behavior, safety invariants, and testing guidance, see
## Preset behavior
- `latest` is an alias to the newest shipped preset.
- Today, `latest -> v3`.
- Today, `latest -> v4`.
- To keep stable behavior over time, pin `preset = "v1"`.
## Precedence
1. `tui.keymap.<context>.<action>`
2. `tui.keymap.global.<action>` (chat/composer fallback actions only)
3. Preset default (`v3` today)
3. Preset default (`v4` today)
## Default `v1` Compatibility Notes
@@ -43,11 +43,15 @@ For runtime behavior, safety invariants, and testing guidance, see
- `queue`: queue current draft while a task is running
- `toggle_shortcuts`: toggle composer shortcut overlay
- `toggle_vim_mode`: toggle Vim mode for composer input
- `reasoning_down`: lower active reasoning level when possible
- `reasoning_up`: raise active reasoning level when possible
### `chat`
- `edit_previous_message`: chat override for edit-previous flow
- `confirm_edit_previous_message`: chat override for edit confirmation
- `reasoning_down`: chat override for lowering active reasoning level
- `reasoning_up`: chat override for raising active reasoning level
### `composer`
+2 -2
View File
@@ -85,8 +85,8 @@ at `https://github.com/openai/codex/blob/main/docs/default-keymap.toml`.
Preset semantics:
1. `latest` is an alias to the newest shipped preset.
2. `v3` is the current baseline; `v1` and `v2` are frozen for historical behavior.
3. Today, `latest -> v3`.
2. `v4` is the current baseline; `v1`, `v2`, and `v3` are frozen for historical behavior.
3. Today, `latest -> v4`.
User guidance: