mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(tui): add keymap debug inspector (#20794)
## Why We constantly get bug reports about keys not being recognized by Codex when the terminal is not handling the key press. Running `/keymap debug` or `/keymap` and going to the Debug tab, we can allow the user to either understand that the key being pressed is not being recognized or to check what it's being recognized as and report or reassign that key. | Menu | Inspector | Hint | |---|---|---| | <img width="1369" height="796" alt="CleanShot 2026-05-02 at 12 57 12" src="https://github.com/user-attachments/assets/512b6faa-344e-4aee-9c00-b4bdc633a662" /> | <img width="1261" height="754" alt="CleanShot 2026-05-02 at 12 56 36" src="https://github.com/user-attachments/assets/a6ddae7d-e174-4ee4-893f-e6bec4fff4ab" /> | <img width="1369" height="796" alt="CleanShot 2026-05-02 at 12 57 30" src="https://github.com/user-attachments/assets/db507784-f40a-4cff-ac23-a61d9703769b" /> | ## Summary - add a Debug tab to `/keymap` and support `/keymap debug` for direct access - show what key Codex receives, the config key representation, raw event details, and matching actions - add a progressive missing-key hint that escalates after a few seconds with no detected keypress ## Validation - `just fmt` - `cargo test -p codex-tui keymap_setup::tests::debug_view` - `cargo test -p codex-tui keymap_setup::tests` - `cargo test -p codex-tui slash_keymap` - `cargo test -p codex-tui` (unit tests passed; integration test `suite::model_availability_nux::resume_startup_does_not_consume_model_availability_nux_count` failed locally by itself with `codex resume` exiting 1 and terminal probe escape output) - `just fix -p codex-tui` - `just argument-comment-lint` - `cargo insta pending-snapshots` - `git diff --check`
This commit is contained in:
@@ -85,6 +85,13 @@ impl ChatWidget {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// Opens the keypress inspector with the current runtime bindings.
|
||||
pub(crate) fn open_keymap_debug(&mut self, runtime_keymap: &RuntimeKeymap) {
|
||||
let view = keymap_setup::build_keymap_debug_view(runtime_keymap, &self.config.tui_keymap);
|
||||
self.bottom_pane.show_view(Box::new(view));
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// Opens the menu that lets the user choose which existing binding to replace.
|
||||
///
|
||||
/// This is only used for actions with multiple effective bindings. The chosen binding is
|
||||
|
||||
@@ -582,6 +582,20 @@ impl ChatWidget {
|
||||
"verbose" => self.add_mcp_output(McpServerStatusDetail::Full),
|
||||
_ => self.add_error_message("Usage: /mcp [verbose]".to_string()),
|
||||
},
|
||||
SlashCommand::Keymap => match trimmed.to_ascii_lowercase().as_str() {
|
||||
"" => self.open_keymap_picker(),
|
||||
"debug" => {
|
||||
match crate::keymap::RuntimeKeymap::from_config(&self.config.tui_keymap) {
|
||||
Ok(runtime_keymap) => self.open_keymap_debug(&runtime_keymap),
|
||||
Err(err) => {
|
||||
self.add_error_message(format!(
|
||||
"Invalid `tui.keymap` configuration: {err}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => self.add_error_message("Usage: /keymap [debug]".to_string()),
|
||||
},
|
||||
SlashCommand::Rename if !trimmed.is_empty() => {
|
||||
if !self.ensure_thread_rename_allowed() {
|
||||
return;
|
||||
|
||||
@@ -1228,6 +1228,103 @@ async fn keymap_capture_can_capture_current_copy_shortcut() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_keymap_capture_can_capture_app_shortcuts() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let runtime_keymap = crate::keymap::RuntimeKeymap::defaults();
|
||||
|
||||
for (key, expected) in [('t', "ctrl-t"), ('l', "ctrl-l"), ('g', "ctrl-g")] {
|
||||
chat.open_keymap_capture(
|
||||
"global".to_string(),
|
||||
"open_transcript".to_string(),
|
||||
crate::app_event::KeymapEditIntent::ReplaceAll,
|
||||
&runtime_keymap,
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char(key), KeyModifiers::CONTROL));
|
||||
|
||||
let AppEvent::KeymapCaptured {
|
||||
context,
|
||||
action,
|
||||
key,
|
||||
intent,
|
||||
} = rx.try_recv().expect("captured key event")
|
||||
else {
|
||||
panic!("expected keymap capture event");
|
||||
};
|
||||
assert_eq!(context, "global");
|
||||
assert_eq!(action, "open_transcript");
|
||||
assert_eq!(key, expected);
|
||||
assert_eq!(intent, crate::app_event::KeymapEditIntent::ReplaceAll);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_keymap_debug_opens_keypress_inspector() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.dispatch_command_with_args(SlashCommand::Keymap, "debug".to_string(), Vec::new());
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(popup.contains("Keypress Inspector"));
|
||||
assert!(popup.contains("Waiting for a keypress"));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(popup.contains("global.copy (Copy)"));
|
||||
assert!(
|
||||
drain_insert_history(&mut rx).is_empty(),
|
||||
"debug inspector should open without transcript messages"
|
||||
);
|
||||
assert!(op_rx.try_recv().is_err(), "expected no core op to be sent");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_keymap_debug_can_inspect_app_shortcuts() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.dispatch_command_with_args(SlashCommand::Keymap, "debug".to_string(), Vec::new());
|
||||
|
||||
for (key, expected_action) in [
|
||||
('t', "global.open_transcript (Open Transcript)"),
|
||||
('l', "global.clear_terminal (Clear Terminal)"),
|
||||
('g', "global.open_external_editor (Open External Editor)"),
|
||||
] {
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char(key), KeyModifiers::CONTROL));
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
popup.contains(expected_action),
|
||||
"expected {expected_action:?} in debug popup for ctrl-{key}, got {popup:?}"
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
drain_insert_history(&mut rx).is_empty(),
|
||||
"debug inspector should not run app shortcut side effects"
|
||||
);
|
||||
assert!(op_rx.try_recv().is_err(), "expected no core op to be sent");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_keymap_invalid_args_show_usage() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
submit_composer_text(&mut chat, "/keymap nope");
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let rendered = cells
|
||||
.iter()
|
||||
.map(|cell| lines_to_single_string(cell))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(
|
||||
rendered.contains("Usage: /keymap [debug]"),
|
||||
"expected usage message, got: {rendered:?}"
|
||||
);
|
||||
assert_eq!(recall_latest_after_clearing(&mut chat), "/keymap nope");
|
||||
assert!(op_rx.try_recv().is_err(), "expected no core op to be sent");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_shortcut_can_be_remapped() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
Reference in New Issue
Block a user