fix(tui): welcome auth inputs paste with Ctrl/Super+V

The API-key, custom-provider, and loopback paste boxes matched
KeyCode::Char without checking modifiers, so Ctrl+V inserted a literal
'v' and no chord read the host clipboard. The arms now read the
clipboard through is_paste_key/is_inline_paste_key (the picker.rs
pattern) and the char fallthrough is gated by is_text_input_key, so
Ctrl+letter chords no longer type.
This commit is contained in:
雷电芽衣 committed 2026-09-29 21:57:19 -04:00
1 parent c56660fe07
commit 1d631a07fa
1 file changed
+82 -2
+82 -2
View File
@@ -2689,11 +2689,25 @@ fn handle_welcome_input(ev: &Event, ctx: &mut WelcomeInputCtx<'_>) -> InputOutco
}
return InputOutcome::Unchanged;
}
if crate::input::key::is_paste_key(key)
|| crate::input::key::is_inline_paste_key(key)
{
if let Some(text) = crate::clipboard::system_clipboard_get() {
let cleaned: String =
text.chars().filter(|c| *c != '\n' && *c != '\r').collect();
ctx.auth_code_input.push_str(&cleaned);
return InputOutcome::Changed;
}
crate::clipboard::log_paste_key_empty_host_clipboard("auth input");
return InputOutcome::Unchanged;
}
if key!(Backspace).matches(key) {
ctx.auth_code_input.pop();
return InputOutcome::Changed;
}
if let crossterm::event::KeyCode::Char(c) = key.code {
if let crossterm::event::KeyCode::Char(c) = key.code
&& crate::input::key::is_text_input_key(key)
{
ctx.auth_code_input.push(c);
return InputOutcome::Changed;
}
@@ -2718,11 +2732,25 @@ fn handle_welcome_input(ev: &Event, ctx: &mut WelcomeInputCtx<'_>) -> InputOutco
}
return InputOutcome::Unchanged;
}
if crate::input::key::is_paste_key(key)
|| crate::input::key::is_inline_paste_key(key)
{
if let Some(text) = crate::clipboard::system_clipboard_get() {
let cleaned: String =
text.chars().filter(|c| *c != '\n' && *c != '\r').collect();
ctx.auth_code_input.push_str(&cleaned);
return InputOutcome::Changed;
}
crate::clipboard::log_paste_key_empty_host_clipboard("auth input");
return InputOutcome::Unchanged;
}
if key!(Backspace).matches(key) {
ctx.auth_code_input.pop();
return InputOutcome::Changed;
}
if let crossterm::event::KeyCode::Char(c) = key.code {
if let crossterm::event::KeyCode::Char(c) = key.code
&& crate::input::key::is_text_input_key(key)
{
ctx.auth_code_input.push(c);
return InputOutcome::Changed;
}
@@ -7601,6 +7629,58 @@ pub(crate) mod tests {
assert!(matches!(outcome, InputOutcome::Changed));
assert_eq!(app.auth_code_input, "token_value");
}
/// Ctrl+V / Super+V paste the host clipboard into every auth input.
#[test]
fn auth_inputs_paste_key_chords_insert_clipboard_text() {
let modes = [
AuthMode::Loopback,
AuthMode::ApiKeyEntry(PlatformLogin(kigi_shell::models::PlatformId::MoonshotCn)),
AuthMode::CustomProviderEntry(crate::app::custom_entry::CustomEntryStep::first(
kigi_shell::models::custom::CustomApi::OpenAi,
)),
];
for mode in modes {
for mods in [KeyModifiers::CONTROL, KeyModifiers::SUPER] {
let mut app = test_app();
app.auth_state = AuthState::Authenticating {
request_seq: 1,
handle: None,
auth_url: None,
mode,
};
crate::clipboard::set_clipboard_probe_hook(
crate::clipboard::ClipboardProbeHook {
text: Some("sk-pasted\nkey".to_owned()),
..Default::default()
},
);
let outcome = app.handle_input(&key_event(KeyCode::Char('v'), mods));
crate::clipboard::clear_clipboard_probe_hook();
assert!(
matches!(outcome, InputOutcome::Changed),
"{mode:?} + {mods:?} must paste, got {outcome:?}"
);
assert_eq!(
app.auth_code_input, "sk-pastedkey",
"newlines stripped and no literal 'v' for {mode:?}"
);
}
}
}
/// Ctrl+letter chords are not text input (the old Ctrl+V inserted 'v').
#[test]
fn auth_inputs_ignore_control_letter_chords() {
let mut app = test_app();
app.auth_state = AuthState::Authenticating {
request_seq: 1,
handle: None,
auth_url: None,
mode: AuthMode::Loopback,
};
let outcome = app.handle_input(&key_event(KeyCode::Char('x'), KeyModifiers::CONTROL));
assert!(matches!(outcome, InputOutcome::Unchanged));
assert!(app.auth_code_input.is_empty());
}
#[test]
fn authenticating_loopback_enter_empty_is_noop() {
let mut app = test_app();