Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 104 additions & 1 deletion codex-rs/tui/src/resume_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,9 @@ async fn run_session_picker_with_loader(
return Ok(sel);
}
}
TuiEvent::Paste(pasted) => {
state.handle_paste(pasted);
}
TuiEvent::Draw | TuiEvent::Resize => {
if let Ok(size) = alt.tui.terminal.size() {
let list_height =
Expand All @@ -489,7 +492,6 @@ async fn run_session_picker_with_loader(
state.open_pending_transcript_if_ready();
}
}
_ => {}
}
}
Some(event) = background_events.next() => {
Expand Down Expand Up @@ -543,6 +545,11 @@ fn picker_cwd_filter(
}
}

fn normalize_pasted_query(pasted: &str) -> Option<String> {
let normalized = pasted.split_whitespace().collect::<Vec<_>>().join(" ");
(!normalized.is_empty()).then_some(normalized)
}

fn spawn_app_server_page_loader(
app_server: AppServerSession,
include_non_interactive: bool,
Expand Down Expand Up @@ -1227,6 +1234,21 @@ impl PickerState {
Ok(None)
}

fn handle_paste(&mut self, pasted: String) {
if self.is_transcript_loading() {
return;
}
let Some(pasted) = normalize_pasted_query(&pasted) else {
return;
};
let mut new_query = self.query.clone();
if !new_query.is_empty() && !new_query.ends_with(char::is_whitespace) {
new_query.push(' ');
}
new_query.push_str(&pasted);
self.set_query(new_query);
}

fn start_initial_load(&mut self) {
self.relative_time_reference = Some(Utc::now());
self.reset_pagination();
Expand Down Expand Up @@ -6218,6 +6240,87 @@ session_picker_view = "dense"
assert!(state.pagination.reached_scan_cap);
}

#[tokio::test]
async fn paste_appends_to_existing_query() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
/*show_all*/ true,
/*filter_cwd*/ None,
SessionPickerAction::Resume,
);
state.query = String::from("resize");

state.handle_paste(String::from("results"));

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(|_| {});
let mut state = PickerState::new(
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
/*show_all*/ true,
/*filter_cwd*/ None,
SessionPickerAction::Resume,
);
state.query = String::from("resize");

state.handle_paste(String::from(" \n\t "));

assert_eq!(state.query, "resize");
}

#[tokio::test]
async fn paste_uses_existing_search_loading_path() {
let recorded_requests: Arc<Mutex<Vec<PageLoadRequest>>> = Arc::new(Mutex::new(Vec::new()));
let request_sink = recorded_requests.clone();
let loader = page_only_loader(move |req: PageLoadRequest| {
request_sink.lock().unwrap().push(req);
});

let mut state = PickerState::new(
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
/*show_all*/ true,
/*filter_cwd*/ None,
SessionPickerAction::Resume,
);
state.reset_pagination();
state.ingest_page(page(
vec![make_row(
"/tmp/start.jsonl",
"2025-01-01T00:00:00Z",
"alpha",
)],
Some("2025-01-02T00:00:00Z"),
/*num_scanned_files*/ 1,
/*reached_scan_cap*/ false,
));
recorded_requests.lock().unwrap().clear();

state.handle_paste(String::from("target"));

let guard = recorded_requests.lock().unwrap();
assert_eq!(state.query, "target");
assert_eq!(guard.len(), 1);
assert!(guard[0].search_token.is_some());
}

#[tokio::test]
async fn esc_with_empty_query_starts_fresh() {
let loader = page_only_loader(|_| {});
Expand Down
Loading