mirror of
https://github.com/openai/codex.git
synced 2026-11-04 00:24:43 +00:00
Support two-stroke TUI key chords (#36511)
## What changed - Accept two-stroke bindings such as `ctrl-x ctrl-s` in TUI keymap configuration while preserving arrays as alternative bindings. - Route chords through the active TUI context, show pending and configured chord hints, and cancel pending chords on `esc`, context changes, or a one-second timeout. - Extend `/keymap` to capture, add, replace, and display key chords. - Reject chords that conflict with active single-key bindings, overlapping-context chords, or reserved terminal shortcuts. ## Testing - Add coverage for chord parsing, validation, matching, timeout and cancellation behavior, context dispatch, key capture, and rendered shortcut hints. GitOrigin-RevId: dfdede03f518d4936ad8621ebf5537026f2b282e
This commit is contained in:
1 parent
a1dd74b535
commit
1e85ca099e
61 files changed
+3503
-660
No files matched your search
@@ -82,7 +82,7 @@ impl ChatWidget {
|
||||
.map(|keymap| keymap.chat.clone())
|
||||
.unwrap_or_else(|| default_keymap.chat.clone());
|
||||
let queued_message_edit_hint_binding = queued_message_edit_hint_binding(
|
||||
&chat_keymap.edit_queued_message,
|
||||
runtime_keymap.as_ref().unwrap_or(&default_keymap),
|
||||
current_terminal_info,
|
||||
);
|
||||
let pet_http_client = codex_http_client::RouteAwareClientPool::new(
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
use super::*;
|
||||
|
||||
impl ChatWidget {
|
||||
pub(crate) fn keymap_contexts(&self) -> crate::keymap::KeymapContextSet {
|
||||
self.bottom_pane.keymap_contexts()
|
||||
}
|
||||
|
||||
pub(crate) fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||
if self.bottom_pane.has_active_view()
|
||||
&& !matches!(
|
||||
|
||||
@@ -16,6 +16,7 @@ use codex_terminal_detection::terminal_info;
|
||||
|
||||
use super::ChatWidget;
|
||||
use super::queued_message_edit_hint_binding;
|
||||
use crate::app_event::KeymapCaptureMode;
|
||||
use crate::app_event::KeymapEditIntent;
|
||||
use crate::keymap::RuntimeKeymap;
|
||||
use crate::keymap_setup;
|
||||
@@ -73,12 +74,14 @@ impl ChatWidget {
|
||||
context: String,
|
||||
action: String,
|
||||
intent: KeymapEditIntent,
|
||||
capture_mode: KeymapCaptureMode,
|
||||
runtime_keymap: &RuntimeKeymap,
|
||||
) {
|
||||
let view = keymap_setup::build_keymap_capture_view(
|
||||
context,
|
||||
action,
|
||||
intent,
|
||||
capture_mode,
|
||||
runtime_keymap,
|
||||
self.app_event_tx.clone(),
|
||||
);
|
||||
@@ -169,10 +172,8 @@ 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.queued_message_edit_hint_binding = queued_message_edit_hint_binding(
|
||||
&self.chat_keymap.edit_queued_message,
|
||||
terminal_info(),
|
||||
);
|
||||
self.queued_message_edit_hint_binding =
|
||||
queued_message_edit_hint_binding(runtime_keymap, terminal_info());
|
||||
self.bottom_pane
|
||||
.set_queued_message_edit_binding(self.queued_message_edit_hint_binding);
|
||||
self.bottom_pane.set_keymap_bindings(runtime_keymap);
|
||||
|
||||
@@ -468,14 +468,14 @@ impl ChatWidget {
|
||||
let items = vec![
|
||||
SelectionItem {
|
||||
name: "Yes".to_string(),
|
||||
display_shortcut: Some(key_hint::plain(KeyCode::Char('y'))),
|
||||
display_shortcut: Some(key_hint::plain(KeyCode::Char('y')).into()),
|
||||
actions: send_actions,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "No".to_string(),
|
||||
display_shortcut: Some(key_hint::plain(KeyCode::Char('n'))),
|
||||
display_shortcut: Some(key_hint::plain(KeyCode::Char('n')).into()),
|
||||
is_default: true,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
expression: popup
|
||||
---
|
||||
Experimental features
|
||||
Toggle experimental features. Changes are saved to config.toml.
|
||||
|
||||
› [x] Shell tool Allow the model to run shell commands.
|
||||
|
||||
Press space to select or ctrl + x enter to save for next conversation
|
||||
@@ -1363,7 +1363,7 @@ async fn restore_thread_input_state_applies_running_state_policy() {
|
||||
async fn alt_up_edits_most_recent_queued_message() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.chat_keymap.edit_queued_message = vec![crate::key_hint::alt(KeyCode::Up)];
|
||||
chat.queued_message_edit_hint_binding = Some(crate::key_hint::alt(KeyCode::Up));
|
||||
chat.queued_message_edit_hint_binding = Some(crate::key_hint::alt(KeyCode::Up).into());
|
||||
chat.bottom_pane
|
||||
.set_queued_message_edit_binding(chat.queued_message_edit_hint_binding);
|
||||
|
||||
@@ -1462,6 +1462,42 @@ async fn shift_left_edits_most_recent_queued_message_in_tmux() {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queued_message_edit_hint_displays_configured_chords() {
|
||||
use codex_config::types::KeybindingSpec;
|
||||
use codex_config::types::KeybindingsSpec;
|
||||
use codex_config::types::TuiKeymap;
|
||||
|
||||
let terminal_info = || TerminalInfo {
|
||||
name: TerminalName::Iterm2,
|
||||
term_program: None,
|
||||
version: None,
|
||||
term: None,
|
||||
multiplexer: None,
|
||||
};
|
||||
let mut config = TuiKeymap::default();
|
||||
config.chat.edit_queued_message = Some(KeybindingsSpec::One(KeybindingSpec(
|
||||
"ctrl-x up".to_string(),
|
||||
)));
|
||||
let keymap = RuntimeKeymap::from_config(&config).expect("valid queued edit chord");
|
||||
|
||||
assert_eq!(
|
||||
queued_message_edit_hint_binding(&keymap, terminal_info()),
|
||||
Some(crate::key_hint::ShortcutHint::Chord {
|
||||
prefix: crate::key_hint::ctrl(KeyCode::Char('x')),
|
||||
completion: crate::key_hint::plain(KeyCode::Up),
|
||||
})
|
||||
);
|
||||
|
||||
let default_keymap = RuntimeKeymap::defaults();
|
||||
assert_eq!(
|
||||
queued_message_edit_hint_binding(&default_keymap, terminal_info()),
|
||||
Some(crate::key_hint::ShortcutHint::Single(crate::key_hint::alt(
|
||||
KeyCode::Up,
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queued_message_edit_binding_mapping_covers_special_terminals_and_tmux() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -1173,7 +1173,7 @@ pub(super) async fn assert_shift_left_edits_most_recent_queued_message_for_termi
|
||||
) {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.queued_message_edit_hint_binding =
|
||||
Some(queued_message_edit_binding_for_terminal(terminal_info));
|
||||
Some(queued_message_edit_binding_for_terminal(terminal_info).into());
|
||||
chat.bottom_pane
|
||||
.set_queued_message_edit_binding(chat.queued_message_edit_hint_binding);
|
||||
|
||||
|
||||
@@ -2896,6 +2896,26 @@ async fn experimental_features_popup_snapshot() {
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert_chatwidget_snapshot!("experimental_features_popup", popup);
|
||||
|
||||
let mut config = codex_config::types::TuiKeymap::default();
|
||||
config.list.accept = Some(codex_config::types::KeybindingsSpec::One(
|
||||
codex_config::types::KeybindingSpec("ctrl-x enter".to_string()),
|
||||
));
|
||||
let keymap = crate::keymap::RuntimeKeymap::from_config(&config)
|
||||
.expect("valid experimental-feature chord");
|
||||
let view = ExperimentalFeaturesView::new(
|
||||
vec![ExperimentalFeatureItem {
|
||||
feature: Feature::ShellTool,
|
||||
name: "Shell tool".to_string(),
|
||||
description: "Allow the model to run shell commands.".to_string(),
|
||||
enabled: true,
|
||||
}],
|
||||
chat.app_event_tx.clone(),
|
||||
keymap.list,
|
||||
);
|
||||
chat.bottom_pane.show_view(Box::new(view));
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert_chatwidget_snapshot!("experimental_features_popup_configured_key_chords", popup);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1780,6 +1780,7 @@ async fn keymap_capture_can_capture_current_copy_shortcut() {
|
||||
"composer".to_string(),
|
||||
"submit".to_string(),
|
||||
crate::app_event::KeymapEditIntent::ReplaceAll,
|
||||
crate::app_event::KeymapCaptureMode::SingleKey,
|
||||
&runtime_keymap,
|
||||
);
|
||||
|
||||
@@ -1814,6 +1815,7 @@ async fn slash_keymap_capture_can_capture_app_shortcuts() {
|
||||
"global".to_string(),
|
||||
"open_transcript".to_string(),
|
||||
crate::app_event::KeymapEditIntent::ReplaceAll,
|
||||
crate::app_event::KeymapCaptureMode::SingleKey,
|
||||
&runtime_keymap,
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user