From 1d631a07fac1c0b1c6fa3fb7467c9e6f7e5b37fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 29 Sep 2026 21:57:19 -0400 Subject: [PATCH] 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. --- crates/codegen/kigi-tui/src/app/app_view.rs | 84 ++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/crates/codegen/kigi-tui/src/app/app_view.rs b/crates/codegen/kigi-tui/src/app/app_view.rs index b26f2cf..2a337a4 100644 --- a/crates/codegen/kigi-tui/src/app/app_view.rs +++ b/crates/codegen/kigi-tui/src/app/app_view.rs @@ -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();