Make keybinding hints use configured keys

This commit is contained in:
Felipe Coury
2026-04-23 15:14:00 -03:00
parent 11f99b3c82
commit de0e2ccdea
22 changed files with 464 additions and 122 deletions
+7 -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 `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<KeybindingsSpec>,
/// Open the originating thread from an approval prompt.
pub open_thread: Option<KeybindingsSpec>,
/// Approve the primary option.
pub approve: Option<KeybindingsSpec>,
/// Approve for session when that option exists.
pub approve_for_session: Option<KeybindingsSpec>,
/// Approve with exec-policy prefix when that option exists.
pub approve_for_prefix: Option<KeybindingsSpec>,
/// Grant permissions for this turn with strict auto review.
pub approve_with_strict_auto_review: Option<KeybindingsSpec>,
/// Decline and provide corrective guidance.
pub decline: Option<KeybindingsSpec>,
/// Cancel an elicitation request.
+30 -3
View File
@@ -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"
}
]
},
@@ -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<dyn Renderable>,
_features: &Features,
approval_keymap: &ApprovalKeymap,
list_keymap: &ListKeymap,
) -> (Vec<ApprovalOption>, 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<ApprovalOption> {
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(),
@@ -394,6 +394,9 @@ pub(crate) struct ChatComposer {
queue_keys: Vec<KeyBinding>,
toggle_shortcuts_keys: Vec<KeyBinding>,
editor_keymap: EditorKeymap,
footer_toggle_shortcuts_key: Option<KeyBinding>,
footer_queue_key: Option<KeyBinding>,
footer_insert_newline_key: Option<KeyBinding>,
footer_external_editor_key: Option<KeyBinding>,
footer_edit_previous_key: Option<KeyBinding>,
footer_show_transcript_key: Option<KeyBinding>,
@@ -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,
+205 -20
View File
@@ -76,6 +76,12 @@ 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) toggle_shortcuts_key: Option<KeyBinding>,
pub(crate) queue_key: Option<KeyBinding>,
pub(crate) insert_newline_key: Option<KeyBinding>,
pub(crate) external_editor_key: Option<KeyBinding>,
pub(crate) edit_previous_key: Option<KeyBinding>,
pub(crate) show_transcript_key: Option<KeyBinding>,
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
@@ -276,21 +282,28 @@ struct LeftSideState {
fn left_side_line(
collaboration_mode_indicator: Option<CollaborationModeIndicator>,
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<CollaborationModeIndicator>,
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<KeyBinding>,
queue_key: Option<KeyBinding>,
external_editor_key: Option<KeyBinding>,
edit_previous_key: Option<KeyBinding>,
show_transcript_key: Option<KeyBinding>,
reasoning_down_key: Option<KeyBinding>,
reasoning_up_key: Option<KeyBinding>,
}
@@ -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<KeyBinding>) -> 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<Line<'static>> {
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,
})
@@ -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,
+15 -2
View File
@@ -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(),
])
}
+36 -23
View File
@@ -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<crate::clipboard_copy::ClipboardLease>,
copy_last_response_binding: Vec<KeyBinding>,
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<String>,
@@ -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()),
@@ -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,
+31 -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 `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<KeyBinding>,
pub(crate) open_thread: Vec<KeyBinding>,
pub(crate) approve: Vec<KeyBinding>,
pub(crate) approve_for_session: Vec<KeyBinding>,
pub(crate) approve_for_prefix: Vec<KeyBinding>,
pub(crate) approve_with_strict_auto_review: Vec<KeyBinding>,
pub(crate) decline: Vec<KeyBinding>,
pub(crate) cancel: Vec<KeyBinding>,
}
@@ -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()),
],
+5 -9
View File
@@ -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()
+10
View File
@@ -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()),
+11 -4
View File
@@ -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(),
])
}
@@ -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
@@ -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
@@ -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
@@ -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
+13 -4
View File
@@ -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<u16>,
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
+6 -5
View File
@@ -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`
+3 -1
View File
@@ -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"]
+4 -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 -> v4`.
- Today, `latest -> v5`.
- 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 (`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
+3 -3
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. `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: