diff --git a/codex-rs/config/src/tui_keymap.rs b/codex-rs/config/src/tui_keymap.rs index a4bf558be7..2f6ebc5548 100644 --- a/codex-rs/config/src/tui_keymap.rs +++ b/codex-rs/config/src/tui_keymap.rs @@ -29,7 +29,7 @@ use std::collections::BTreeMap; pub enum TuiKeymapPreset { /// Pointer alias to the latest shipped preset. /// - /// Today this resolves to `v4`. + /// Today this resolves to `v5`. #[default] Latest, /// Frozen initial configurable keymap defaults. @@ -40,6 +40,8 @@ pub enum TuiKeymapPreset { V3, /// Current defaults with reasoning shortcuts exposed through keymap config. V4, + /// Current defaults with approval thread and strict-review shortcuts exposed. + V5, } /// Normalized string representation of a keybinding (for example `ctrl-a`). @@ -329,12 +331,16 @@ pub struct TuiListKeymap { pub struct TuiApprovalKeymap { /// Open the full-screen approval details view. pub open_fullscreen: Option, + /// Open the originating thread from an approval prompt. + pub open_thread: Option, /// Approve the primary option. pub approve: Option, /// Approve for session when that option exists. pub approve_for_session: Option, /// Approve with exec-policy prefix when that option exists. pub approve_for_prefix: Option, + /// Grant permissions for this turn with strict auto review. + pub approve_with_strict_auto_review: Option, /// Decline and provide corrective guidance. pub decline: Option, /// Cancel an elicitation request. diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index ca06a05428..f10d5eece8 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2198,9 +2198,11 @@ "approve": null, "approve_for_prefix": null, "approve_for_session": null, + "approve_with_strict_auto_review": null, "cancel": null, "decline": null, - "open_fullscreen": null + "open_fullscreen": null, + "open_thread": null }, "chat": { "confirm_edit_previous_message": null, @@ -2416,6 +2418,14 @@ ], "description": "Approve for session when that option exists." }, + "approve_with_strict_auto_review": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Grant permissions for this turn with strict auto review." + }, "cancel": { "allOf": [ { @@ -2439,6 +2449,14 @@ } ], "description": "Open the full-screen approval details view." + }, + "open_thread": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Open the originating thread from an approval prompt." } }, "type": "object" @@ -2757,9 +2775,11 @@ "approve": null, "approve_for_prefix": null, "approve_for_session": null, + "approve_with_strict_auto_review": null, "cancel": null, "decline": null, - "open_fullscreen": null + "open_fullscreen": null, + "open_thread": null } }, "chat": { @@ -2952,7 +2972,7 @@ "description": "Versioned keymap defaults.", "oneOf": [ { - "description": "Pointer alias to the latest shipped preset.\n\nToday this resolves to `v4`.", + "description": "Pointer alias to the latest shipped preset.\n\nToday this resolves to `v5`.", "enum": [ "latest" ], @@ -2985,6 +3005,13 @@ "v4" ], "type": "string" + }, + { + "description": "Current defaults with approval thread and strict-review shortcuts exposed.", + "enum": [ + "v5" + ], + "type": "string" } ] }, diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 67548100b7..5a4ce56cb0 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -30,6 +30,7 @@ use crate::key_hint::KeyBinding; use crate::key_hint::KeyBindingListExt; use crate::keymap::ApprovalKeymap; use crate::keymap::ListKeymap; +use crate::keymap::primary_binding; use crate::render::highlight::highlight_bash_to_lines; use crate::render::renderable::ColumnRenderable; use crate::render::renderable::Renderable; @@ -210,8 +211,13 @@ impl ApprovalOverlay { fn set_current(&mut self, request: ApprovalRequest) { self.current_complete = false; let header = build_header(&request); - let (options, params) = - Self::build_options(&request, header, &self.features, &self.approval_keymap); + let (options, params) = Self::build_options( + &request, + header, + &self.features, + &self.approval_keymap, + &self.list_keymap, + ); self.current_request = Some(request); self.options = options; self.list = @@ -223,6 +229,7 @@ impl ApprovalOverlay { header: Box, _features: &Features, approval_keymap: &ApprovalKeymap, + list_keymap: &ListKeymap, ) -> (Vec, SelectionViewParams) { let (options, title) = match request { ApprovalRequest::Exec { @@ -278,7 +285,7 @@ impl ApprovalOverlay { .collect(); let params = SelectionViewParams { - footer_hint: Some(approval_footer_hint(request)), + footer_hint: Some(approval_footer_hint(request, list_keymap, approval_keymap)), items, header, ..Default::default() @@ -453,7 +460,7 @@ impl ApprovalOverlay { } if key_event.kind == KeyEventKind::Press - && matches!(key_event.code, KeyCode::Char('o')) + && self.approval_keymap.open_thread.is_pressed(*key_event) && let Some(request) = self.current_request.as_ref() && request.thread_label().is_some() { @@ -560,20 +567,26 @@ impl Renderable for ApprovalOverlay { } } -fn approval_footer_hint(request: &ApprovalRequest) -> Line<'static> { +fn approval_footer_hint( + request: &ApprovalRequest, + list_keymap: &ListKeymap, + approval_keymap: &ApprovalKeymap, +) -> Line<'static> { + let accept = + primary_binding(&list_keymap.accept).unwrap_or_else(|| key_hint::plain(KeyCode::Enter)); + let cancel = + primary_binding(&list_keymap.cancel).unwrap_or_else(|| key_hint::plain(KeyCode::Esc)); let mut spans = vec![ "Press ".into(), - key_hint::plain(KeyCode::Enter).into(), + accept.into(), " to confirm or ".into(), - key_hint::plain(KeyCode::Esc).into(), + cancel.into(), " to cancel".into(), ]; - if request.thread_label().is_some() { - spans.extend([ - " or ".into(), - key_hint::plain(KeyCode::Char('o')).into(), - " to open thread".into(), - ]); + if request.thread_label().is_some() + && let Some(open_thread) = primary_binding(&approval_keymap.open_thread) + { + spans.extend([" or ".into(), open_thread.into(), " to open thread".into()]); } Line::from(spans) } @@ -933,7 +946,7 @@ fn permissions_options(keymap: &ApprovalKeymap) -> Vec { decision: ApprovalDecision::Permissions( PermissionsDecision::GrantForTurnWithStrictAutoReview, ), - shortcuts: vec![key_hint::plain(KeyCode::Char('r'))], + shortcuts: keymap.approve_with_strict_auto_review.clone(), }, ApprovalOption { label: "Yes, grant these permissions for this session".to_string(), diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 66e8fde985..79ea727b88 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -394,6 +394,9 @@ pub(crate) struct ChatComposer { queue_keys: Vec, toggle_shortcuts_keys: Vec, editor_keymap: EditorKeymap, + footer_toggle_shortcuts_key: Option, + footer_queue_key: Option, + footer_insert_newline_key: Option, footer_external_editor_key: Option, footer_edit_previous_key: Option, footer_show_transcript_key: Option, @@ -553,6 +556,13 @@ impl ChatComposer { key_hint::shift(KeyCode::Char('?')), ], editor_keymap: RuntimeKeymap::defaults().editor, + footer_toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + footer_queue_key: Some(key_hint::plain(KeyCode::Tab)), + footer_insert_newline_key: Some(if use_shift_enter_hint { + key_hint::shift(KeyCode::Enter) + } else { + key_hint::ctrl(KeyCode::Char('j')) + }), 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'))), @@ -637,6 +647,15 @@ impl ChatComposer { self.toggle_shortcuts_keys = keymap.composer.toggle_shortcuts.clone(); self.editor_keymap = keymap.editor.clone(); self.textarea.set_keymap_bindings(keymap); + self.footer_toggle_shortcuts_key = primary_binding(&keymap.composer.toggle_shortcuts); + self.footer_queue_key = primary_binding(&keymap.composer.queue); + let shift_enter = key_hint::shift(KeyCode::Enter); + self.footer_insert_newline_key = + if self.use_shift_enter_hint && keymap.editor.insert_newline.contains(&shift_enter) { + Some(shift_enter) + } else { + primary_binding(&keymap.editor.insert_newline) + }; 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); @@ -3324,6 +3343,12 @@ impl ChatComposer { is_wsl, status_line_value: self.status_line_value.clone(), status_line_enabled: self.status_line_enabled, + toggle_shortcuts_key: self.footer_toggle_shortcuts_key, + queue_key: self.footer_queue_key, + insert_newline_key: self.footer_insert_newline_key, + external_editor_key: self.footer_external_editor_key, + edit_previous_key: self.footer_edit_previous_key, + show_transcript_key: self.footer_show_transcript_key, reasoning_down_key: self.footer_reasoning_down_key, reasoning_up_key: self.footer_reasoning_up_key, active_agent_label: self.active_agent_label.clone(), @@ -4146,6 +4171,7 @@ impl ChatComposer { Some(single_line_footer_layout( hint_rect, right_width, + &footer_props, left_mode_indicator, show_cycle_hint, show_shortcuts_hint, diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index aa9dbbc132..e0f10c29e8 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -76,6 +76,12 @@ pub(crate) struct FooterProps { pub(crate) quit_shortcut_key: KeyBinding, pub(crate) status_line_value: Option>, pub(crate) status_line_enabled: bool, + pub(crate) toggle_shortcuts_key: Option, + pub(crate) queue_key: Option, + pub(crate) insert_newline_key: Option, + pub(crate) external_editor_key: Option, + pub(crate) edit_previous_key: Option, + pub(crate) show_transcript_key: Option, pub(crate) reasoning_down_key: Option, pub(crate) reasoning_up_key: Option, /// Active thread label shown when the footer is rendering contextual information instead of an @@ -276,21 +282,28 @@ struct LeftSideState { fn left_side_line( collaboration_mode_indicator: Option, state: LeftSideState, + props: &FooterProps, ) -> Line<'static> { let mut line = Line::from(""); match state.hint { SummaryHintKind::None => {} SummaryHintKind::Shortcuts => { - line.push_span(key_hint::plain(KeyCode::Char('?'))); - line.push_span(" for shortcuts".dim()); + if let Some(key) = props.toggle_shortcuts_key { + line.push_span(key); + line.push_span(" for shortcuts".dim()); + } } SummaryHintKind::QueueMessage => { - line.push_span(key_hint::plain(KeyCode::Tab)); - line.push_span(" to queue message".dim()); + if let Some(key) = props.queue_key { + line.push_span(key); + line.push_span(" to queue message".dim()); + } } SummaryHintKind::QueueShort => { - line.push_span(key_hint::plain(KeyCode::Tab)); - line.push_span(" to queue".dim()); + if let Some(key) = props.queue_key { + line.push_span(key); + line.push_span(" to queue".dim()); + } } }; @@ -315,6 +328,7 @@ pub(crate) enum SummaryLeft { pub(crate) fn single_line_footer_layout( area: Rect, context_width: u16, + props: &FooterProps, collaboration_mode_indicator: Option, show_cycle_hint: bool, show_shortcuts_hint: bool, @@ -331,7 +345,7 @@ pub(crate) fn single_line_footer_layout( hint: hint_kind, show_cycle_hint, }; - let default_line = left_side_line(collaboration_mode_indicator, default_state); + let default_line = left_side_line(collaboration_mode_indicator, default_state, props); let default_width = default_line.width() as u16; if default_width > 0 && can_show_left_with_context(area, default_width, context_width) { return (SummaryLeft::Default, true); @@ -341,7 +355,7 @@ pub(crate) fn single_line_footer_layout( if state == default_state { default_line.clone() } else { - left_side_line(collaboration_mode_indicator, state) + left_side_line(collaboration_mode_indicator, state, props) } }; let state_width = |state: LeftSideState| -> u16 { state_line(state).width() as u16 }; @@ -450,7 +464,8 @@ pub(crate) fn single_line_footer_layout( // Compute the width without going through `state_line` so we do not // depend on `default_state` (which may still be a queue variant). let mode_only_width = - left_side_line(Some(collaboration_mode_indicator), mode_only_state).width() as u16; + left_side_line(Some(collaboration_mode_indicator), mode_only_state, props).width() + as u16; if !context_requires_cycle_hint && can_show_left_with_context(area, mode_only_width, context_width) { @@ -458,6 +473,7 @@ pub(crate) fn single_line_footer_layout( SummaryLeft::Custom(left_side_line( Some(collaboration_mode_indicator), mode_only_state, + props, )), true, // show_context ); @@ -467,6 +483,7 @@ pub(crate) fn single_line_footer_layout( SummaryLeft::Custom(left_side_line( Some(collaboration_mode_indicator), mode_only_state, + props, )), false, // show_context ); @@ -616,7 +633,7 @@ fn footer_from_props_lines( }, show_cycle_hint, }; - vec![left_side_line(collaboration_mode_indicator, state)] + vec![left_side_line(collaboration_mode_indicator, state, props)] } FooterMode::ShortcutOverlay => { let state = ShortcutsState { @@ -624,12 +641,20 @@ fn footer_from_props_lines( esc_backtrack_hint: props.esc_backtrack_hint, is_wsl: props.is_wsl, collaboration_modes_enabled: props.collaboration_modes_enabled, + insert_newline_key: props.insert_newline_key, + queue_key: props.queue_key, + external_editor_key: props.external_editor_key, + edit_previous_key: props.edit_previous_key, + show_transcript_key: props.show_transcript_key, reasoning_down_key: props.reasoning_down_key, reasoning_up_key: props.reasoning_up_key, }; shortcut_overlay_lines(state) } - FooterMode::EscHint => vec![esc_hint_line(props.esc_backtrack_hint)], + FooterMode::EscHint => vec![esc_hint_line( + props.esc_backtrack_hint, + props.edit_previous_key, + )], FooterMode::ComposerHasDraft => { let state = LeftSideState { hint: if show_queue_hint { @@ -641,7 +666,7 @@ fn footer_from_props_lines( }, show_cycle_hint, }; - vec![left_side_line(collaboration_mode_indicator, state)] + vec![left_side_line(collaboration_mode_indicator, state, props)] } } } @@ -743,6 +768,11 @@ struct ShortcutsState { esc_backtrack_hint: bool, is_wsl: bool, collaboration_modes_enabled: bool, + insert_newline_key: Option, + queue_key: Option, + external_editor_key: Option, + edit_previous_key: Option, + show_transcript_key: Option, reasoning_down_key: Option, reasoning_up_key: Option, } @@ -751,15 +781,15 @@ fn quit_shortcut_reminder_line(key: KeyBinding) -> Line<'static> { Line::from(vec![key.into(), " again to quit".into()]).dim() } -fn esc_hint_line(esc_backtrack_hint: bool) -> Line<'static> { - let esc = key_hint::plain(KeyCode::Esc); +fn esc_hint_line(esc_backtrack_hint: bool, key: Option) -> Line<'static> { + let key = key.unwrap_or_else(|| key_hint::plain(KeyCode::Esc)); if esc_backtrack_hint { - Line::from(vec![esc.into(), " again to edit previous message".into()]).dim() + Line::from(vec![key.into(), " again to edit previous message".into()]).dim() } else { Line::from(vec![ - esc.into(), + key.into(), " ".into(), - esc.into(), + key.into(), " to edit previous message".into(), ]) .dim() @@ -957,8 +987,25 @@ impl ShortcutDescriptor { } fn overlay_entry(&self, state: ShortcutsState) -> Option> { - let binding = self.binding_for(state)?; - let mut line = Line::from(vec![self.prefix.into(), binding.key.into()]); + let key = match self.id { + ShortcutId::InsertNewline => state + .insert_newline_key + .or_else(|| self.binding_for(state).map(|binding| binding.key)), + ShortcutId::QueueMessageTab => state + .queue_key + .or_else(|| self.binding_for(state).map(|binding| binding.key)), + ShortcutId::ExternalEditor => state + .external_editor_key + .or_else(|| self.binding_for(state).map(|binding| binding.key)), + ShortcutId::EditPrevious => state + .edit_previous_key + .or_else(|| self.binding_for(state).map(|binding| binding.key)), + ShortcutId::ShowTranscript => state + .show_transcript_key + .or_else(|| self.binding_for(state).map(|binding| binding.key)), + _ => self.binding_for(state).map(|binding| binding.key), + }?; + let mut line = Line::from(vec![self.prefix.into(), key.into()]); match self.id { ShortcutId::EditPrevious => { if state.esc_backtrack_hint { @@ -966,7 +1013,7 @@ impl ShortcutDescriptor { } else { line.extend(vec![ " ".into(), - key_hint::plain(KeyCode::Esc).into(), + key.into(), " to edit previous message".into(), ]); } @@ -1248,6 +1295,7 @@ mod tests { let (summary_left, show_context) = single_line_footer_layout( area, right_width, + props, left_mode_indicator, show_cycle_hint, show_shortcuts_hint, @@ -1366,6 +1414,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: false, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1384,6 +1438,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: false, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::shift(KeyCode::Enter)), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1402,6 +1462,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: false, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1420,6 +1486,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: false, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1438,6 +1510,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: false, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1456,6 +1534,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: false, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1474,6 +1558,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: false, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1492,6 +1582,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: false, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1512,6 +1608,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: false, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1532,6 +1634,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: false, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1548,6 +1656,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: false, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1577,6 +1691,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: false, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1599,6 +1719,12 @@ 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, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1616,6 +1742,12 @@ 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, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1633,6 +1765,12 @@ 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, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1650,6 +1788,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, // command timed out / empty status_line_enabled: true, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1673,6 +1817,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: false, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1696,6 +1846,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: true, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1722,6 +1878,12 @@ mod tests { "Status line content that should truncate before the mode indicator".to_string(), )), status_line_enabled: true, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1745,6 +1907,12 @@ mod tests { quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), status_line_value: None, status_line_enabled: false, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), 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()), @@ -1762,6 +1930,12 @@ 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, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), 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()), @@ -1785,6 +1959,12 @@ mod tests { .to_string(), )), status_line_enabled: true, + toggle_shortcuts_key: Some(key_hint::plain(KeyCode::Char('?'))), + queue_key: Some(key_hint::plain(KeyCode::Tab)), + insert_newline_key: Some(key_hint::ctrl(KeyCode::Char('j'))), + external_editor_key: Some(key_hint::ctrl(KeyCode::Char('g'))), + edit_previous_key: Some(key_hint::plain(KeyCode::Esc)), + show_transcript_key: Some(key_hint::ctrl(KeyCode::Char('t'))), reasoning_down_key: Some(key_hint::alt(KeyCode::Char(','))), reasoning_up_key: Some(key_hint::alt(KeyCode::Char('.'))), active_agent_label: None, @@ -1841,6 +2021,11 @@ mod tests { esc_backtrack_hint: false, is_wsl, collaboration_modes_enabled: false, + insert_newline_key: None, + queue_key: None, + external_editor_key: None, + edit_previous_key: None, + show_transcript_key: None, reasoning_down_key: None, reasoning_up_key: None, }) diff --git a/codex-rs/tui/src/bottom_pane/list_selection_view.rs b/codex-rs/tui/src/bottom_pane/list_selection_view.rs index be5d6c4954..9503658056 100644 --- a/codex-rs/tui/src/bottom_pane/list_selection_view.rs +++ b/codex-rs/tui/src/bottom_pane/list_selection_view.rs @@ -1417,6 +1417,7 @@ mod tests { /*current_name*/ None, Some(&codex_home), Some(94), + &crate::keymap::RuntimeKeymap::defaults().list, ); let view = new_view(params, tx); @@ -1430,6 +1431,7 @@ mod tests { /*current_name*/ None, /*codex_home*/ None, Some(120), + &crate::keymap::RuntimeKeymap::defaults().list, ); assert!( params.preserve_side_content_bg, diff --git a/codex-rs/tui/src/bottom_pane/popup_consts.rs b/codex-rs/tui/src/bottom_pane/popup_consts.rs index 2cabe389b1..0ecf683c16 100644 --- a/codex-rs/tui/src/bottom_pane/popup_consts.rs +++ b/codex-rs/tui/src/bottom_pane/popup_consts.rs @@ -4,6 +4,8 @@ use crossterm::event::KeyCode; use ratatui::text::Line; use crate::key_hint; +use crate::keymap::ListKeymap; +use crate::keymap::primary_binding; /// Maximum number of rows any popup should attempt to display. /// Keep this consistent across all popups for a uniform feel. @@ -11,11 +13,22 @@ pub(crate) const MAX_POPUP_ROWS: usize = 8; /// Standard footer hint text used by popups. pub(crate) fn standard_popup_hint_line() -> Line<'static> { + standard_popup_hint_line_for_keymap(&ListKeymap { + move_up: Vec::new(), + move_down: Vec::new(), + accept: vec![key_hint::plain(KeyCode::Enter)], + cancel: vec![key_hint::plain(KeyCode::Esc)], + }) +} + +pub(crate) fn standard_popup_hint_line_for_keymap(keymap: &ListKeymap) -> Line<'static> { + let accept = primary_binding(&keymap.accept).unwrap_or_else(|| key_hint::plain(KeyCode::Enter)); + let cancel = primary_binding(&keymap.cancel).unwrap_or_else(|| key_hint::plain(KeyCode::Esc)); Line::from(vec![ "Press ".into(), - key_hint::plain(KeyCode::Enter).into(), + accept.into(), " to confirm or ".into(), - key_hint::plain(KeyCode::Esc).into(), + cancel.into(), " to go back".into(), ]) } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index ba8ddc470c..57787548f6 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -334,7 +334,7 @@ use crate::bottom_pane::SelectionAction; use crate::bottom_pane::SelectionItem; use crate::bottom_pane::SelectionViewParams; use crate::bottom_pane::custom_prompt_view::CustomPromptView; -use crate::bottom_pane::popup_consts::standard_popup_hint_line; +use crate::bottom_pane::popup_consts::standard_popup_hint_line_for_keymap; use crate::clipboard_paste::paste_image_to_temp_png; use crate::collaboration_modes; use crate::diff_render::display_path_for; @@ -356,6 +356,7 @@ use crate::key_hint; use crate::key_hint::KeyBinding; use crate::key_hint::KeyBindingListExt; use crate::keymap::ChatKeymap; +use crate::keymap::ListKeymap; use crate::keymap::RuntimeKeymap; use crate::keymap_setup; #[cfg(test)] @@ -826,6 +827,7 @@ pub(crate) struct ChatWidget { clipboard_lease: Option, copy_last_response_binding: Vec, chat_keymap: ChatKeymap, + list_keymap: ListKeymap, /// Raw markdown of the most recently completed agent response that /// survived any local thread rollback. last_agent_markdown: Option, @@ -2784,7 +2786,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { title: Some(MULTI_AGENT_ENABLE_TITLE.to_string()), subtitle: Some("Subagents are currently disabled in your config.".to_string()), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, ..Default::default() }); @@ -2834,7 +2836,7 @@ impl ChatWidget { "Learn more: ".dim(), MEMORIES_DOC_URL.cyan().underlined(), ])), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, ..Default::default() }); @@ -5168,6 +5170,10 @@ impl ChatWidget { .as_ref() .map(|keymap| keymap.chat.clone()) .unwrap_or_else(|| RuntimeKeymap::defaults().chat); + let list_keymap = runtime_keymap + .as_ref() + .map(|keymap| keymap.list.clone()) + .unwrap_or_else(|| RuntimeKeymap::defaults().list); let mut widget = Self { app_event_tx: app_event_tx.clone(), @@ -5212,6 +5218,7 @@ impl ChatWidget { clipboard_lease: None, copy_last_response_binding, chat_keymap, + list_keymap, running_commands: HashMap::new(), collab_agent_metadata: HashMap::new(), pending_collab_spawn_requests: HashMap::new(), @@ -7782,6 +7789,7 @@ impl ChatWidget { self.config.tui_theme.as_deref(), codex_home.as_deref(), terminal_width, + &self.list_keymap, ); self.bottom_pane.show_selection_view(params); } @@ -7853,10 +7861,15 @@ 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.list_keymap = runtime_keymap.list.clone(); self.bottom_pane.set_keymap_bindings(runtime_keymap); self.request_redraw(); } + fn standard_popup_hint_line(&self) -> Line<'static> { + standard_popup_hint_line_for_keymap(&self.list_keymap) + } + pub(crate) fn return_to_keymap_picker( &mut self, context: &str, @@ -8166,7 +8179,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { title: Some("Approaching rate limits".to_string()), subtitle: Some(format!("Switch to {switch_model} for lower credit usage?")), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, ..Default::default() }); @@ -8212,7 +8225,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { title: Some(title.to_string()), subtitle: Some(prompt.to_string()), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, initial_selected_idx: Some(1), ..Default::default() @@ -8361,7 +8374,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { header: Box::new(header), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, ..Default::default() }); @@ -8394,7 +8407,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { title: Some("Settings".to_string()), subtitle: Some("Configure settings for Codex.".to_string()), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, ..Default::default() }); @@ -8479,7 +8492,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { header: Box::new(header), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, ..Default::default() }); @@ -8516,7 +8529,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { header: Box::new(header), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, ..Default::default() }); @@ -8639,7 +8652,7 @@ impl ChatWidget { "Pick a quick auto mode or browse all models.", ); self.bottom_pane.show_selection_view(SelectionViewParams { - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, header, ..Default::default() @@ -8744,7 +8757,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { title: Some("Select Collaboration Mode".to_string()), subtitle: Some("Pick a collaboration preset.".to_string()), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, ..Default::default() }); @@ -8855,7 +8868,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { title: Some(PLAN_MODE_REASONING_SCOPE_TITLE.to_string()), subtitle: Some(subtitle), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items: vec![ SelectionItem { name: PLAN_MODE_REASONING_SCOPE_PLAN_ONLY.to_string(), @@ -9039,7 +9052,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { header: Box::new(header), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, initial_selected_idx, ..Default::default() @@ -9277,7 +9290,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { title: Some("Update Model Permissions".to_string()), footer_note, - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, header: Box::new(()), ..Default::default() @@ -9480,7 +9493,7 @@ impl ChatWidget { ]; self.bottom_pane.show_selection_view(SelectionViewParams { - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, header: Box::new(header), ..Default::default() @@ -9591,7 +9604,7 @@ impl ChatWidget { ]; self.bottom_pane.show_selection_view(SelectionViewParams { - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, header: Box::new(header), ..Default::default() @@ -9651,7 +9664,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { title: None, - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, header: Box::new(header), ..Default::default() @@ -9728,7 +9741,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { title: None, - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, header: Box::new(header), ..Default::default() @@ -9819,7 +9832,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { title: None, - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, header: Box::new(header), ..Default::default() @@ -11280,7 +11293,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { title: Some("Select a review preset".into()), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, ..Default::default() }); @@ -11313,7 +11326,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { title: Some("Select a base branch".to_string()), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, is_searchable: true, search_placeholder: Some("Type to search branches".to_string()), @@ -11349,7 +11362,7 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { title: Some("Select a commit to review".to_string()), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(self.standard_popup_hint_line()), items, is_searchable: true, search_placeholder: Some("Type to search commits".to_string()), @@ -11731,7 +11744,7 @@ pub(crate) fn show_review_commit_picker_with_entries( chat.bottom_pane.show_selection_view(SelectionViewParams { title: Some("Select a commit to review".to_string()), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(chat.standard_popup_hint_line()), items, is_searchable: true, search_placeholder: Some("Type to search commits".to_string()), diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index ef24f74b36..d7c83e082d 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -213,6 +213,7 @@ pub(super) async fn make_chatwidget_manual( clipboard_lease: None, copy_last_response_binding: RuntimeKeymap::defaults().app.copy, chat_keymap: RuntimeKeymap::defaults().chat, + list_keymap: RuntimeKeymap::defaults().list, pending_guardian_review_status: PendingGuardianReviewStatus::default(), terminal_title_status_kind: TerminalTitleStatusKind::Working, last_agent_markdown: None, diff --git a/codex-rs/tui/src/keymap.rs b/codex-rs/tui/src/keymap.rs index f442f72e38..60b67d63bf 100644 --- a/codex-rs/tui/src/keymap.rs +++ b/codex-rs/tui/src/keymap.rs @@ -33,7 +33,7 @@ use std::collections::HashMap; /// /// 1. Context-specific binding (`tui.keymap.`). /// 2. `tui.keymap.global` for actions that support global fallback. -/// 3. Built-in preset defaults (`latest` currently points to `v4`). +/// 3. Built-in preset defaults (`latest` currently points to `v5`). #[derive(Clone, Debug)] pub(crate) struct RuntimeKeymap { pub(crate) app: AppKeymap, @@ -207,9 +207,11 @@ pub(crate) struct ListKeymap { #[derive(Clone, Debug)] pub(crate) struct ApprovalKeymap { pub(crate) open_fullscreen: Vec, + pub(crate) open_thread: Vec, pub(crate) approve: Vec, pub(crate) approve_for_session: Vec, pub(crate) approve_for_prefix: Vec, + pub(crate) approve_with_strict_auto_review: Vec, pub(crate) decline: Vec, pub(crate) cancel: Vec, } @@ -489,9 +491,16 @@ impl RuntimeKeymap { let approval = ApprovalKeymap { open_fullscreen: resolve_local!(keymap, defaults, approval, open_fullscreen), + open_thread: resolve_local!(keymap, defaults, approval, open_thread), approve: resolve_local!(keymap, defaults, approval, approve), approve_for_session: resolve_local!(keymap, defaults, approval, approve_for_session), approve_for_prefix: resolve_local!(keymap, defaults, approval, approve_for_prefix), + approve_with_strict_auto_review: resolve_local!( + keymap, + defaults, + approval, + approve_with_strict_auto_review + ), decline: resolve_local!(keymap, defaults, approval, decline), cancel: resolve_local!(keymap, defaults, approval, cancel), }; @@ -527,7 +536,8 @@ impl RuntimeKeymap { fn defaults_for_preset(preset: TuiKeymapPreset) -> Self { match preset { - TuiKeymapPreset::Latest | TuiKeymapPreset::V4 => Self::defaults_v4(), + TuiKeymapPreset::Latest | TuiKeymapPreset::V5 => Self::defaults_v5(), + TuiKeymapPreset::V4 => Self::defaults_v4(), TuiKeymapPreset::V3 => Self::defaults_v3(), TuiKeymapPreset::V2 => Self::defaults_v2(), TuiKeymapPreset::V1 => Self::defaults_v1(), @@ -703,9 +713,11 @@ impl RuntimeKeymap { KeyModifiers::CONTROL | KeyModifiers::SHIFT, )) ], + open_thread: default_bindings![], approve: default_bindings![plain(KeyCode::Char('y'))], approve_for_session: default_bindings![plain(KeyCode::Char('a'))], approve_for_prefix: default_bindings![plain(KeyCode::Char('p'))], + approve_with_strict_auto_review: default_bindings![], decline: default_bindings![plain(KeyCode::Esc), plain(KeyCode::Char('n'))], cancel: default_bindings![plain(KeyCode::Char('c'))], }, @@ -771,6 +783,18 @@ impl RuntimeKeymap { defaults } + /// Current keymap defaults for preset `v5`. + /// + /// This keeps earlier presets intact while moving approval-only shortcuts + /// into configurable keymap defaults. + fn defaults_v5() -> Self { + let mut defaults = Self::defaults_v4(); + defaults.approval.open_thread = default_bindings![plain(KeyCode::Char('o'))]; + defaults.approval.approve_with_strict_auto_review = + default_bindings![plain(KeyCode::Char('r'))]; + defaults + } + /// Reject ambiguous bindings in scopes that are evaluated together. /// /// We validate in multiple passes because runtime handling has mixed @@ -990,6 +1014,7 @@ impl RuntimeKeymap { "approval", [ ("open_fullscreen", self.approval.open_fullscreen.as_slice()), + ("open_thread", self.approval.open_thread.as_slice()), ("approve", self.approval.approve.as_slice()), ( "approve_for_session", @@ -999,6 +1024,10 @@ impl RuntimeKeymap { "approve_for_prefix", self.approval.approve_for_prefix.as_slice(), ), + ( + "approve_with_strict_auto_review", + self.approval.approve_with_strict_auto_review.as_slice(), + ), ("decline", self.approval.decline.as_slice()), ("cancel", self.approval.cancel.as_slice()), ], diff --git a/codex-rs/tui/src/keymap_setup.rs b/codex-rs/tui/src/keymap_setup.rs index 3f6f1f329b..8eebf7d12a 100644 --- a/codex-rs/tui/src/keymap_setup.rs +++ b/codex-rs/tui/src/keymap_setup.rs @@ -34,6 +34,7 @@ use crate::bottom_pane::ColumnWidthMode; use crate::bottom_pane::SelectionItem; use crate::bottom_pane::SelectionViewParams; use crate::bottom_pane::popup_consts::standard_popup_hint_line; +use crate::bottom_pane::popup_consts::standard_popup_hint_line_for_keymap; use crate::keymap::RuntimeKeymap; use crate::render::renderable::ColumnRenderable; use crate::render::renderable::Renderable; @@ -66,13 +67,8 @@ fn key_binding_span(binding: &str) -> ratatui::text::Span<'static> { } } -fn keymap_action_menu_hint_line() -> Line<'static> { - Line::from(vec![ - "enter".cyan(), - " select · ".dim(), - "esc".cyan(), - " back".dim(), - ]) +fn keymap_action_menu_hint_line(runtime_keymap: &RuntimeKeymap) -> Line<'static> { + standard_popup_hint_line_for_keymap(&runtime_keymap.list) } fn open_capture_action( @@ -264,7 +260,7 @@ pub(crate) fn build_keymap_action_menu_params( "`tui.keymap.*`".cyan(), " override.".dim(), ])), - footer_hint: Some(keymap_action_menu_hint_line()), + footer_hint: Some(keymap_action_menu_hint_line(runtime_keymap)), items, col_width_mode: ColumnWidthMode::Fixed, ..Default::default() @@ -315,7 +311,7 @@ pub(crate) fn build_keymap_replace_binding_menu_params( SelectionViewParams { view_id: Some(KEYMAP_REPLACE_BINDING_MENU_VIEW_ID), header: Box::new(header), - footer_hint: Some(keymap_action_menu_hint_line()), + footer_hint: Some(keymap_action_menu_hint_line(runtime_keymap)), items, col_width_mode: ColumnWidthMode::Fixed, ..Default::default() diff --git a/codex-rs/tui/src/keymap_setup/actions.rs b/codex-rs/tui/src/keymap_setup/actions.rs index 6c33777775..f4a1cb7e05 100644 --- a/codex-rs/tui/src/keymap_setup/actions.rs +++ b/codex-rs/tui/src/keymap_setup/actions.rs @@ -111,9 +111,11 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[ action("list", "List", "accept", "Accept the current list selection."), action("list", "List", "cancel", "Cancel and close selection views."), action("approval", "Approval", "open_fullscreen", "Open approval details fullscreen."), + action("approval", "Approval", "open_thread", "Open the originating thread from an approval prompt."), action("approval", "Approval", "approve", "Approve the primary option."), action("approval", "Approval", "approve_for_session", "Approve for the session when available."), action("approval", "Approval", "approve_for_prefix", "Approve with an exec-policy prefix when available."), + action("approval", "Approval", "approve_with_strict_auto_review", "Grant permissions for this turn with strict auto review."), action("approval", "Approval", "decline", "Decline and provide corrective guidance."), action("approval", "Approval", "cancel", "Cancel an elicitation request."), action("onboarding", "Onboarding", "move_up", "Move onboarding selection up."), @@ -227,9 +229,13 @@ pub(super) fn binding_slot<'a>( ("list", "accept") => Some(&mut keymap.list.accept), ("list", "cancel") => Some(&mut keymap.list.cancel), ("approval", "open_fullscreen") => Some(&mut keymap.approval.open_fullscreen), + ("approval", "open_thread") => Some(&mut keymap.approval.open_thread), ("approval", "approve") => Some(&mut keymap.approval.approve), ("approval", "approve_for_session") => Some(&mut keymap.approval.approve_for_session), ("approval", "approve_for_prefix") => Some(&mut keymap.approval.approve_for_prefix), + ("approval", "approve_with_strict_auto_review") => { + Some(&mut keymap.approval.approve_with_strict_auto_review) + } ("approval", "decline") => Some(&mut keymap.approval.decline), ("approval", "cancel") => Some(&mut keymap.approval.cancel), ("onboarding", "move_up") => Some(&mut keymap.onboarding.move_up), @@ -331,9 +337,13 @@ pub(super) fn bindings_for_action<'a>( ("list", "accept") => Some(runtime_keymap.list.accept.as_slice()), ("list", "cancel") => Some(runtime_keymap.list.cancel.as_slice()), ("approval", "open_fullscreen") => Some(runtime_keymap.approval.open_fullscreen.as_slice()), + ("approval", "open_thread") => Some(runtime_keymap.approval.open_thread.as_slice()), ("approval", "approve") => Some(runtime_keymap.approval.approve.as_slice()), ("approval", "approve_for_session") => Some(runtime_keymap.approval.approve_for_session.as_slice()), ("approval", "approve_for_prefix") => Some(runtime_keymap.approval.approve_for_prefix.as_slice()), + ("approval", "approve_with_strict_auto_review") => { + Some(runtime_keymap.approval.approve_with_strict_auto_review.as_slice()) + } ("approval", "decline") => Some(runtime_keymap.approval.decline.as_slice()), ("approval", "cancel") => Some(runtime_keymap.approval.cancel.as_slice()), ("onboarding", "move_up") => Some(runtime_keymap.onboarding.move_up.as_slice()), diff --git a/codex-rs/tui/src/keymap_setup/picker.rs b/codex-rs/tui/src/keymap_setup/picker.rs index 6d6a1009bb..0c017812b2 100644 --- a/codex-rs/tui/src/keymap_setup/picker.rs +++ b/codex-rs/tui/src/keymap_setup/picker.rs @@ -1,6 +1,7 @@ //! Shortcut picker construction for `/keymap`. use codex_config::types::TuiKeymap; +use crossterm::event::KeyCode; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; @@ -12,7 +13,9 @@ use crate::bottom_pane::SelectionItem; use crate::bottom_pane::SelectionRowDisplay; use crate::bottom_pane::SelectionTab; use crate::bottom_pane::SelectionViewParams; +use crate::key_hint; use crate::keymap::RuntimeKeymap; +use crate::keymap::primary_binding; use crate::render::renderable::ColumnRenderable; use crate::render::renderable::Renderable; @@ -243,7 +246,7 @@ fn build_keymap_picker_params_for_action( SelectionViewParams { view_id: Some(KEYMAP_PICKER_VIEW_ID), header: Box::new(()), - footer_hint: Some(keymap_picker_hint_line()), + footer_hint: Some(keymap_picker_hint_line(runtime_keymap)), tabs, initial_tab_id: Some(KEYMAP_ALL_TAB_ID.to_string()), is_searchable: true, @@ -380,17 +383,21 @@ fn action_count_line(count: usize) -> String { } } -fn keymap_picker_hint_line() -> Line<'static> { +fn keymap_picker_hint_line(runtime_keymap: &RuntimeKeymap) -> Line<'static> { + let accept = primary_binding(&runtime_keymap.list.accept) + .unwrap_or_else(|| key_hint::plain(KeyCode::Enter)); + let cancel = primary_binding(&runtime_keymap.list.cancel) + .unwrap_or_else(|| key_hint::plain(KeyCode::Esc)); Line::from(vec![ "left/right".cyan(), " group · ".dim(), - "enter".cyan(), + accept.into(), " edit shortcut · ".dim(), "*".cyan(), " custom · ".dim(), "-".cyan(), " unbound · ".dim(), - "esc".cyan(), + cancel.into(), " close".dim(), ]) } diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap index 83d7edc38f..b6477eaaa5 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap @@ -5,19 +5,19 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 93 actions, 1 customized, 1 unbound. + 95 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-. +› 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 diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap index 5dafce1474..273be66a18 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap @@ -2,7 +2,7 @@ source: tui/src/keymap_setup.rs expression: snapshot --- -tab: All (93 selectable) +tab: All (95 selectable) tab: Common (21 selectable) tab: Customized (0) (0 selectable) tab: Unbound (1) (1 selectable) @@ -11,7 +11,7 @@ tab: Composer (3 selectable) tab: Editor (16 selectable) tab: Vim (34 selectable) tab: Navigation (17 selectable) -tab: Approval (6 selectable) +tab: Approval (8 selectable) Open Transcript | ctrl-t | Global open_transcript Open Transcript Open the transcript overlay. ctrl-t Default Open External Editor | ctrl-g | Global open_external_editor Open External Editor Open the current draft in an external editor. ctrl-g Default Copy | ctrl-o | Global copy Copy Copy the last agent response to the clipboard. ctrl-o Default diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap index 8d1a8fcdf9..01f7184d6e 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap @@ -5,20 +5,20 @@ expression: "render_picker(params, 78)" Keymap All configurable shortcuts. - 93 actions, 0 customized, 1 unbound. + 95 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-. +› 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 diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap index d7cd3440ee..a9f253e75f 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap @@ -5,19 +5,19 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 93 actions, 0 customized, 1 unbound. + 95 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-. +› 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 diff --git a/codex-rs/tui/src/theme_picker.rs b/codex-rs/tui/src/theme_picker.rs index 563054a37f..43d25c2cca 100644 --- a/codex-rs/tui/src/theme_picker.rs +++ b/codex-rs/tui/src/theme_picker.rs @@ -25,7 +25,7 @@ use crate::app_event::AppEvent; use crate::bottom_pane::SelectionItem; use crate::bottom_pane::SelectionViewParams; use crate::bottom_pane::SideContentWidth; -use crate::bottom_pane::popup_consts::standard_popup_hint_line; +use crate::bottom_pane::popup_consts::standard_popup_hint_line_for_keymap; use crate::bottom_pane::popup_content_width; use crate::bottom_pane::side_by_side_layout_widths; use crate::diff_render::DiffLineType; @@ -33,6 +33,7 @@ use crate::diff_render::current_diff_render_style_context; use crate::diff_render::line_number_width; use crate::diff_render::push_wrapped_diff_line_with_style_context; use crate::diff_render::push_wrapped_diff_line_with_syntax_and_style_context; +use crate::keymap::ListKeymap; use crate::render::highlight; use crate::render::renderable::Renderable; use crate::status::format_directory_display; @@ -315,6 +316,7 @@ pub(crate) fn build_theme_picker_params( current_name: Option<&str>, codex_home: Option<&Path>, terminal_width: Option, + list_keymap: &ListKeymap, ) -> SelectionViewParams { // Snapshot the current theme so we can restore on cancel. let original_theme = highlight::current_syntax_theme(); @@ -390,7 +392,7 @@ pub(crate) fn build_theme_picker_params( codex_home_owned.as_deref(), terminal_width, )), - footer_hint: Some(standard_popup_hint_line()), + footer_hint: Some(standard_popup_hint_line_for_keymap(list_keymap)), items, is_searchable: true, search_placeholder: Some("Type to filter themes...".to_string()), @@ -477,7 +479,10 @@ mod tests { #[test] fn theme_picker_uses_half_width_with_stacked_fallback_preview() { let params = build_theme_picker_params( - /*current_name*/ None, /*codex_home*/ None, /*terminal_width*/ None, + /*current_name*/ None, + /*codex_home*/ None, + /*terminal_width*/ None, + &crate::keymap::RuntimeKeymap::defaults().list, ); assert_eq!(params.side_content_width, SideContentWidth::Half); assert_eq!(params.side_content_min_width, WIDE_PREVIEW_MIN_WIDTH); @@ -487,7 +492,10 @@ mod tests { #[test] fn theme_picker_items_include_search_values_for_preview_mapping() { let params = build_theme_picker_params( - /*current_name*/ None, /*codex_home*/ None, /*terminal_width*/ None, + /*current_name*/ None, + /*codex_home*/ None, + /*terminal_width*/ None, + &crate::keymap::RuntimeKeymap::defaults().list, ); assert!( params.items.iter().all(|item| item.search_value.is_some()), @@ -640,6 +648,7 @@ mod tests { Some("not-a-real-theme"), /*codex_home*/ None, Some(120), + &crate::keymap::RuntimeKeymap::defaults().list, ); let selected_idx = params .initial_selected_idx diff --git a/docs/config.md b/docs/config.md index 8bb590d1ce..39a8347a92 100644 --- a/docs/config.md +++ b/docs/config.md @@ -36,13 +36,14 @@ Precedence is applied in this order (highest first): ### Presets -- `latest`: moving alias for the newest preset; today `latest -> v4` -- `v4`: current defaults, exposing reasoning shortcuts (`alt-,` / `alt-.`) in the keymap +- `latest`: moving alias for the newest preset; today `latest -> v5` +- `v5`: current defaults, exposing approval thread-open and strict auto-review shortcuts in the keymap +- `v4`: exposes 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 -When defaults change in the future, a new version (for example `v4`) is added and +When defaults change in the future, a new version (for example `v6`) is added and `latest` may move to it. Pin to `v1` if you want stable historical behavior. TODO(docs): mirror this preset migration note on developers.openai.com. @@ -72,8 +73,8 @@ TODO(docs): mirror this preset migration note on developers.openai.com. `half_page_down`, `jump_top`, `jump_bottom`, `close`, `close_transcript`, `edit_previous_message`, `edit_next_message`, `confirm_edit_message` - `list`: `move_up`, `move_down`, `accept`, `cancel` -- `approval`: `open_fullscreen`, `approve`, `approve_for_session`, - `approve_for_prefix`, `decline`, `cancel` +- `approval`: `open_fullscreen`, `open_thread`, `approve`, `approve_for_session`, + `approve_for_prefix`, `approve_with_strict_auto_review`, `decline`, `cancel` - `onboarding`: `move_up`, `move_down`, `select_first`, `select_second`, `select_third`, `confirm`, `cancel`, `quit`, `toggle_animation` diff --git a/docs/default-keymap.toml b/docs/default-keymap.toml index c3921d7f5a..aeb0ceafc5 100644 --- a/docs/default-keymap.toml +++ b/docs/default-keymap.toml @@ -18,7 +18,7 @@ [tui.keymap] # Preset alias for built-in defaults. -# "latest" currently points to "v4". +# "latest" currently points to "v5". preset = "latest" [tui.keymap.global] @@ -149,9 +149,11 @@ cancel = ["esc"] # Approval modal actions. # Include ctrl+shift fallback for terminals that preserve SHIFT on Ctrl letter chords. open_fullscreen = ["ctrl-a", "ctrl-shift-a"] +open_thread = ["o"] approve = ["y"] approve_for_session = ["a"] approve_for_prefix = ["p"] +approve_with_strict_auto_review = ["r"] decline = ["esc", "n"] cancel = ["c"] diff --git a/docs/keymap-action-matrix.md b/docs/keymap-action-matrix.md index 010d51dab9..c932288fcc 100644 --- a/docs/keymap-action-matrix.md +++ b/docs/keymap-action-matrix.md @@ -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 -> v4`. +- Today, `latest -> v5`. - To keep stable behavior over time, pin `preset = "v1"`. ## Precedence 1. `tui.keymap..` 2. `tui.keymap.global.` (chat/composer fallback actions only) -3. Preset default (`v4` today) +3. Preset default (`v5` today) ## Default `v1` Compatibility Notes @@ -116,9 +116,11 @@ For runtime behavior, safety invariants, and testing guidance, see ### `approval` - `open_fullscreen`: open full-screen approval details +- `open_thread`: open the originating thread from an approval prompt - `approve`: approve primary request - `approve_for_session`: approve-for-session option - `approve_for_prefix`: approve-for-prefix option +- `approve_with_strict_auto_review`: grant permissions for this turn with strict auto review - `decline`: decline request - `cancel`: cancel elicitation request - MCP elicitation safety rule: `Esc` is always treated as `cancel` (never diff --git a/docs/tui-keymap.md b/docs/tui-keymap.md index 72090ba733..28314a254e 100644 --- a/docs/tui-keymap.md +++ b/docs/tui-keymap.md @@ -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. `v4` is the current baseline; `v1`, `v2`, and `v3` are frozen for historical behavior. -3. Today, `latest -> v4`. +2. `v5` is the current baseline; `v1`, `v2`, `v3`, and `v4` are frozen for historical behavior. +3. Today, `latest -> v5`. User guidance: @@ -96,7 +96,7 @@ User guidance: Developer policy: 1. Do not mutate old preset defaults after release. -2. Add a new version (for example `v4`) for behavior changes. +2. Add a new version (for example `v6`) for behavior changes. 3. Update docs and migration notes whenever `latest` changes. Migration notes: