fix(tui): route bracketed paste to provider picker key entry instead of composer (#342)

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.
This commit is contained in:
Hunter Bown
2026-05-02 02:33:26 -05:00
parent 47bb91a9b7
commit 42eea19066
3 changed files with 24 additions and 0 deletions
+10
View File
@@ -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 {
+4
View File
@@ -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);
+10
View File
@@ -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<ViewEvent> {
let action = self
.views