[feat] make approval key matching case insensitive (#1862)

This commit is contained in:
ae
2025-08-05 15:50:06 -07:00
committed by GitHub
parent 42bd73e150
commit f6c8d1117c

View File

@@ -47,6 +47,8 @@ pub(crate) enum ApprovalRequest {
}
/// Options displayed in the *select* mode.
///
/// The `key` is matched case-insensitively.
struct SelectOption {
label: Line<'static>,
description: &'static str,
@@ -187,6 +189,16 @@ impl UserApprovalWidget<'_> {
}
}
/// Normalize a key for comparison.
/// - For `KeyCode::Char`, converts to lowercase for case-insensitive matching.
/// - Other key codes are returned unchanged.
fn normalize_keycode(code: KeyCode) -> KeyCode {
match code {
KeyCode::Char(c) => KeyCode::Char(c.to_ascii_lowercase()),
other => other,
}
}
/// Handle Ctrl-C pressed by the user while the modal is visible.
/// Behaves like pressing Escape: abort the request and close the modal.
pub(crate) fn on_ctrl_c(&mut self) {
@@ -210,7 +222,12 @@ impl UserApprovalWidget<'_> {
self.send_decision(ReviewDecision::Abort);
}
other => {
if let Some(opt) = self.select_options.iter().find(|opt| opt.key == other) {
let normalized = Self::normalize_keycode(other);
if let Some(opt) = self
.select_options
.iter()
.find(|opt| Self::normalize_keycode(opt.key) == normalized)
{
self.send_decision(opt.decision);
}
}
@@ -330,3 +347,59 @@ impl WidgetRef for &UserApprovalWidget<'_> {
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
use std::path::PathBuf;
use std::sync::mpsc::channel;
#[test]
fn lowercase_shortcut_is_accepted() {
let (tx_raw, rx) = channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let req = ApprovalRequest::Exec {
id: "1".to_string(),
command: vec!["echo".to_string()],
cwd: PathBuf::new(),
reason: None,
};
let mut widget = UserApprovalWidget::new(req, tx);
widget.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
assert!(widget.is_complete());
let events: Vec<AppEvent> = rx.try_iter().collect();
assert!(events.iter().any(|e| matches!(
e,
AppEvent::CodexOp(Op::ExecApproval {
decision: ReviewDecision::Approved,
..
})
)));
}
#[test]
fn uppercase_shortcut_is_accepted() {
let (tx_raw, rx) = channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let req = ApprovalRequest::Exec {
id: "2".to_string(),
command: vec!["echo".to_string()],
cwd: PathBuf::new(),
reason: None,
};
let mut widget = UserApprovalWidget::new(req, tx);
widget.handle_key_event(KeyEvent::new(KeyCode::Char('Y'), KeyModifiers::NONE));
assert!(widget.is_complete());
let events: Vec<AppEvent> = rx.try_iter().collect();
assert!(events.iter().any(|e| matches!(
e,
AppEvent::CodexOp(Op::ExecApproval {
decision: ReviewDecision::Approved,
..
})
)));
}
}