From 42eea19066f5504f8750ee3bd3cbba9f5153c8af Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sat, 2 May 2026 02:33:26 -0500 Subject: [PATCH] fix(tui): route bracketed paste to provider picker key entry instead of composer (#342) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add handle_paste(text) -> ViewAction method to the ModalView trait with a default no-op. ProviderPickerView overrides it in KeyEntry stage to sanitize and append pasted text to api_key_input (rejecting whitespace in the same way as the Char handler). Wire into the Event::Paste handler in ui.rs: before falling through to app.insert_paste_text(), check view_stack.handle_paste(). If the top modal consumes the paste, skip the composer entirely. If a modal is open but does NOT consume the paste, also skip the composer — any modal that receives paste while focused should handle it, not leak into the chat input. --- crates/tui/src/tui/provider_picker.rs | 10 ++++++++++ crates/tui/src/tui/ui.rs | 4 ++++ crates/tui/src/tui/views/mod.rs | 10 ++++++++++ 3 files changed, 24 insertions(+) diff --git a/crates/tui/src/tui/provider_picker.rs b/crates/tui/src/tui/provider_picker.rs index 00dc14e2..c43a41f1 100644 --- a/crates/tui/src/tui/provider_picker.rs +++ b/crates/tui/src/tui/provider_picker.rs @@ -246,6 +246,16 @@ impl ModalView for ProviderPickerView { self } + fn handle_paste(&mut self, text: &str) -> ViewAction { + if self.stage == Stage::KeyEntry { + let sanitized: String = text.chars().filter(|c| !c.is_whitespace()).collect(); + if !sanitized.is_empty() { + self.api_key_input.push_str(&sanitized); + } + } + ViewAction::None + } + fn handle_key(&mut self, key: KeyEvent) -> ViewAction { match self.stage { Stage::List => match key.code { diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 74e03596..53258cb6 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -1240,6 +1240,10 @@ async fn run_event_loop( sync_api_key_validation_status(app, false); } else if app.is_history_search_active() { app.history_search_insert_str(text); + } else if app.view_stack.handle_paste(text) { + // Modal consumed the paste (e.g. provider picker key entry) + } else if !app.view_stack.is_empty() { + // A non-consumed modal is open — don't leak paste into composer } else { // Paste into main input app.insert_paste_text(text); diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs index b3ed31c1..4b700ea7 100644 --- a/crates/tui/src/tui/views/mod.rs +++ b/crates/tui/src/tui/views/mod.rs @@ -173,6 +173,9 @@ pub enum ViewAction { pub trait ModalView: std::any::Any { fn kind(&self) -> ModalKind; fn handle_key(&mut self, key: KeyEvent) -> ViewAction; + fn handle_paste(&mut self, _text: &str) -> ViewAction { + ViewAction::None + } fn handle_mouse(&mut self, _mouse: MouseEvent) -> ViewAction { ViewAction::None } @@ -245,6 +248,13 @@ impl ViewStack { self.apply_action(action) } + pub fn handle_paste(&mut self, text: &str) -> bool { + self.views + .last_mut() + .map(|view| matches!(view.handle_paste(text), ViewAction::None)) + .unwrap_or(false) + } + pub fn handle_mouse(&mut self, mouse: MouseEvent) -> Vec { let action = self .views