diff --git a/codex-rs/tui/src/bottom_pane/list_selection_view.rs b/codex-rs/tui/src/bottom_pane/list_selection_view.rs index 94ef15cde..317d26965 100644 --- a/codex-rs/tui/src/bottom_pane/list_selection_view.rs +++ b/codex-rs/tui/src/bottom_pane/list_selection_view.rs @@ -15,6 +15,7 @@ use ratatui::widgets::Widget; use super::selection_popup_common::render_menu_surface; use super::selection_popup_common::wrap_styled_line; use crate::app_event_sender::AppEventSender; +use crate::clipboard_paste::normalize_pasted_search_query; use crate::key_hint::KeyBinding; use crate::key_hint::KeyBindingListExt; use crate::key_hint::is_plain_text_key_event; @@ -1016,6 +1017,18 @@ impl BottomPaneView for ListSelectionView { } } + fn handle_paste(&mut self, pasted: String) -> bool { + if !self.is_searchable { + return false; + } + let Some(pasted) = normalize_pasted_search_query(&pasted) else { + return false; + }; + self.search_query.push_str(&pasted); + self.apply_filter(); + true + } + fn is_complete(&self) -> bool { self.completion.is_some() } @@ -1672,6 +1685,61 @@ mod tests { ); } + #[test] + fn paste_appends_to_search_query_and_filters_items() { + let (tx_raw, _rx) = unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let mut view = new_view( + SelectionViewParams { + items: vec![ + SelectionItem { + name: "main -> feature/other".to_string(), + search_value: Some("feature/other".to_string()), + ..Default::default() + }, + SelectionItem { + name: "main -> feature/paste-support".to_string(), + search_value: Some("feature/paste-support".to_string()), + ..Default::default() + }, + ], + is_searchable: true, + ..Default::default() + }, + tx, + ); + view.handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE)); + + assert!(view.handle_paste("eature/paste-support\n".to_string())); + + assert_eq!(view.search_query, "feature/paste-support"); + assert_eq!(view.filtered_indices, vec![1]); + assert_eq!(view.selected_actual_idx(), Some(1)); + } + + #[test] + fn whitespace_only_paste_is_ignored() { + let (tx_raw, _rx) = unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let mut view = new_view( + SelectionViewParams { + items: vec![SelectionItem { + name: "main".to_string(), + search_value: Some("main".to_string()), + ..Default::default() + }], + is_searchable: true, + ..Default::default() + }, + tx, + ); + + assert!(!view.handle_paste(" \n\t ".to_string())); + + assert_eq!(view.search_query, ""); + assert_eq!(view.filtered_indices, vec![0]); + } + #[test] fn switching_tabs_changes_visible_items_and_clears_search() { let (tx_raw, _rx) = unbounded_channel::(); diff --git a/codex-rs/tui/src/clipboard_paste.rs b/codex-rs/tui/src/clipboard_paste.rs index 4d28b365f..55ca6ca65 100644 --- a/codex-rs/tui/src/clipboard_paste.rs +++ b/codex-rs/tui/src/clipboard_paste.rs @@ -236,6 +236,12 @@ pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImag )) } +/// Normalize pasted text for a single-line search query. +pub(crate) fn normalize_pasted_search_query(pasted: &str) -> Option { + let normalized = pasted.split_whitespace().collect::>().join(" "); + (!normalized.is_empty()).then_some(normalized) +} + /// Normalize pasted text that may represent a filesystem path. /// /// Supports: @@ -368,6 +374,19 @@ pub fn pasted_image_format(path: &Path) -> EncodedImageFormat { } } +#[cfg(test)] +mod pasted_search_query_tests { + use super::*; + + #[test] + fn collapses_whitespace() { + assert_eq!( + normalize_pasted_search_query(" alpha\n\tbeta\r\n gamma "), + Some(String::from("alpha beta gamma")) + ); + } +} + #[cfg(test)] mod pasted_paths_tests { use super::*; diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 6ae9f37a2..c261ade7d 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -7,6 +7,7 @@ use std::sync::Arc; mod transcript; use crate::app_server_session::AppServerSession; +use crate::clipboard_paste::normalize_pasted_search_query; use crate::color::blend; use crate::color::is_light; use crate::git_action_directives::parse_assistant_markdown; @@ -548,11 +549,6 @@ fn picker_cwd_filter( } } -fn normalize_pasted_query(pasted: &str) -> Option { - let normalized = pasted.split_whitespace().collect::>().join(" "); - (!normalized.is_empty()).then_some(normalized) -} - fn spawn_app_server_page_loader( app_server: AppServerSession, include_non_interactive: bool, @@ -1237,7 +1233,7 @@ impl PickerState { if self.is_transcript_loading() { return; } - let Some(pasted) = normalize_pasted_query(&pasted) else { + let Some(pasted) = normalize_pasted_search_query(&pasted) else { return; }; let mut new_query = self.query.clone(); @@ -6228,14 +6224,6 @@ session_picker_view = "dense" assert_eq!(state.query, "resize results"); } - #[test] - fn normalize_pasted_query_collapses_whitespace() { - assert_eq!( - normalize_pasted_query(" alpha\n\tbeta\r\n gamma "), - Some(String::from("alpha beta gamma")) - ); - } - #[tokio::test] async fn whitespace_only_paste_is_ignored() { let loader = page_only_loader(|_| {});