From cb8dc18a64d1457d660357bae6bf7cdca896dca8 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 2 Apr 2026 14:02:52 -0600 Subject: [PATCH 1/4] Fix resume picker initial loading state (#16591) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses #16514 Problem: Resume picker could show “No sessions yet” before the initial session fetch finished. Solution: Render a loading message while the first page is pending, and keep the empty state for truly empty results. --- codex-rs/tui/src/resume_picker.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index d5e7fbd1ef..60d189d226 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -1360,11 +1360,10 @@ fn render_empty_state_line(state: &PickerState) -> Line<'static> { return vec!["No results for your search".italic().dim()].into(); } - if state.all_rows.is_empty() && state.pagination.num_scanned_files == 0 { - return vec!["No sessions yet".italic().dim()].into(); - } - if state.pagination.loading.is_pending() { + if state.all_rows.is_empty() && state.pagination.num_scanned_files == 0 { + return vec!["Loading sessions…".italic().dim()].into(); + } return vec!["Loading older sessions…".italic().dim()].into(); } From 30ee9e769e8f32eb0f57399fec5c18a981438d53 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 2 Apr 2026 13:16:16 -0700 Subject: [PATCH 2/4] fix: increase another startup timeout for PowerShell (#16613) --- codex-rs/core/src/models_manager/manager_tests.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/models_manager/manager_tests.rs b/codex-rs/core/src/models_manager/manager_tests.rs index 3e0ca152c7..6ab1c267bb 100644 --- a/codex-rs/core/src/models_manager/manager_tests.rs +++ b/codex-rs/core/src/models_manager/manager_tests.rs @@ -171,10 +171,16 @@ $lines | Select-Object -Skip 1 | Set-Content -Path tokens.txt } fn auth_config(&self) -> ModelProviderAuthInfo { + let timeout_ms = if cfg!(windows) { + // `powershell.exe` startup can be slow on loaded Windows CI workers + 10_000 + } else { + 2_000 + }; ModelProviderAuthInfo { command: self.command.clone(), args: self.args.clone(), - timeout_ms: NonZeroU64::new(/*value*/ 1_000).unwrap(), + timeout_ms: NonZeroU64::new(timeout_ms).unwrap(), refresh_interval_ms: 60_000, cwd: match codex_utils_absolute_path::AbsolutePathBuf::try_from(self.tempdir.path()) { Ok(cwd) => cwd, From c0f2fed67e54cd6ca700e94161989f2f338adf8d Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 2 Apr 2026 14:22:57 -0600 Subject: [PATCH 3/4] Fix resume picker stale thread names (#16601) Addresses #16562 Problem: Resume picker could keep a stale backend-provided thread title instead of the latest name from session_index.jsonl. Solution: Always backfill/override picker row names from local session_index.jsonl and cover stale-name replacement with a regression test. --- codex-rs/tui/src/resume_picker.rs | 63 ++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 60d189d226..0b7c504677 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -791,9 +791,6 @@ impl PickerState { let Some(thread_id) = row.thread_id else { continue; }; - if row.thread_name.is_some() { - continue; - } if self.thread_name_cache.contains_key(&thread_id) { continue; } @@ -817,11 +814,14 @@ impl PickerState { let Some(thread_id) = row.thread_id else { continue; }; - let thread_name = self.thread_name_cache.get(&thread_id).cloned().flatten(); - if row.thread_name == thread_name { + let Some(thread_name) = self.thread_name_cache.get(&thread_id).cloned().flatten() + else { + continue; + }; + if row.thread_name.as_ref() == Some(&thread_name) { continue; } - row.thread_name = thread_name; + row.thread_name = Some(thread_name); updated = true; } @@ -2275,6 +2275,57 @@ mod tests { assert_snapshot!("resume_picker_thread_names", snapshot); } + #[tokio::test] + async fn update_thread_names_prefers_local_session_index_names() { + let tempdir = tempfile::tempdir().expect("tempdir"); + let thread_id = + ThreadId::from_string("11111111-1111-1111-1111-111111111111").expect("thread id"); + let session_index_entry = json!({ + "id": thread_id, + "thread_name": "Saved session name", + "updated_at": "2025-01-01T00:00:00Z", + }); + std::fs::write( + tempdir.path().join("session_index.jsonl"), + format!("{session_index_entry}\n"), + ) + .expect("write session index"); + + let loader: PageLoader = Arc::new(|_| {}); + let mut state = PickerState::new( + tempdir.path().to_path_buf(), + FrameRequester::test_dummy(), + loader, + ProviderFilter::MatchDefault(String::from("openai")), + /*show_all*/ true, + /*filter_cwd*/ None, + SessionPickerAction::Resume, + ); + + state.all_rows = vec![Row { + path: Some(PathBuf::from("/tmp/a.jsonl")), + preview: String::from("First prompt"), + thread_id: Some(thread_id), + thread_name: Some(String::from("stale backend title")), + created_at: None, + updated_at: None, + cwd: None, + git_branch: None, + }]; + state.filtered_rows = state.all_rows.clone(); + + state.update_thread_names().await; + + assert_eq!( + state.all_rows[0].thread_name, + Some(String::from("Saved session name")) + ); + assert_eq!( + state.filtered_rows[0].display_preview(), + "Saved session name" + ); + } + #[test] fn pageless_scrolling_deduplicates_and_keeps_order() { let loader: PageLoader = Arc::new(|_| {}); From c623a321744c0a65ade691c302cc72d6c533f2c9 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 2 Apr 2026 13:26:27 -0700 Subject: [PATCH 4/4] fix: add shell fallback paths for pwsh/powershell that work on GitHub Actions Windows runners --- codex-rs/core/src/shell.rs | 48 ++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/codex-rs/core/src/shell.rs b/codex-rs/core/src/shell.rs index 1943712565..48c760d667 100644 --- a/codex-rs/core/src/shell.rs +++ b/codex-rs/core/src/shell.rs @@ -166,7 +166,7 @@ fn get_shell_path( shell_type: ShellType, provided_path: Option<&PathBuf>, binary_name: &str, - fallback_paths: Vec<&str>, + fallback_paths: &[&str], ) -> Option { // If exact provided path exists, use it if provided_path.and_then(file_exists).is_some() { @@ -197,8 +197,10 @@ fn get_shell_path( None } +const ZSH_FALLBACK_PATHS: &[&str] = &["/bin/zsh"]; + fn get_zsh_shell(path: Option<&PathBuf>) -> Option { - let shell_path = get_shell_path(ShellType::Zsh, path, "zsh", vec!["/bin/zsh"]); + let shell_path = get_shell_path(ShellType::Zsh, path, "zsh", ZSH_FALLBACK_PATHS); shell_path.map(|shell_path| Shell { shell_type: ShellType::Zsh, @@ -207,8 +209,10 @@ fn get_zsh_shell(path: Option<&PathBuf>) -> Option { }) } +const BASH_FALLBACK_PATHS: &[&str] = &["/bin/bash"]; + fn get_bash_shell(path: Option<&PathBuf>) -> Option { - let shell_path = get_shell_path(ShellType::Bash, path, "bash", vec!["/bin/bash"]); + let shell_path = get_shell_path(ShellType::Bash, path, "bash", BASH_FALLBACK_PATHS); shell_path.map(|shell_path| Shell { shell_type: ShellType::Bash, @@ -217,8 +221,10 @@ fn get_bash_shell(path: Option<&PathBuf>) -> Option { }) } +const SH_FALLBACK_PATHS: &[&str] = &["/bin/sh"]; + fn get_sh_shell(path: Option<&PathBuf>) -> Option { - let shell_path = get_shell_path(ShellType::Sh, path, "sh", vec!["/bin/sh"]); + let shell_path = get_shell_path(ShellType::Sh, path, "sh", SH_FALLBACK_PATHS); shell_path.map(|shell_path| Shell { shell_type: ShellType::Sh, @@ -227,14 +233,32 @@ fn get_sh_shell(path: Option<&PathBuf>) -> Option { }) } +// Note the `pwsh` and `powershell` fallback paths are where the respective +// shells are commonly installed on GitHub Actions Windows runners, but may not +// be present on all Windows machines: +// https://docs.github.com/en/actions/tutorials/build-and-test-code/powershell + +#[cfg(windows)] +const PWSH_FALLBACK_PATHS: &[&str] = &[r#"C:\Program Files\PowerShell\7\pwsh.exe"#]; +#[cfg(not(windows))] +const PWSH_FALLBACK_PATHS: &[&str] = &["/usr/local/bin/pwsh"]; + +#[cfg(windows)] +const POWERSHELL_FALLBACK_PATHS: &[&str] = + &[r#"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"#]; +#[cfg(not(windows))] +const POWERSHELL_FALLBACK_PATHS: &[&str] = &[]; + fn get_powershell_shell(path: Option<&PathBuf>) -> Option { - let shell_path = get_shell_path( - ShellType::PowerShell, - path, - "pwsh", - vec!["/usr/local/bin/pwsh"], - ) - .or_else(|| get_shell_path(ShellType::PowerShell, path, "powershell", vec![])); + let shell_path = get_shell_path(ShellType::PowerShell, path, "pwsh", PWSH_FALLBACK_PATHS) + .or_else(|| { + get_shell_path( + ShellType::PowerShell, + path, + "powershell", + POWERSHELL_FALLBACK_PATHS, + ) + }); shell_path.map(|shell_path| Shell { shell_type: ShellType::PowerShell, @@ -244,7 +268,7 @@ fn get_powershell_shell(path: Option<&PathBuf>) -> Option { } fn get_cmd_shell(path: Option<&PathBuf>) -> Option { - let shell_path = get_shell_path(ShellType::Cmd, path, "cmd", vec![]); + let shell_path = get_shell_path(ShellType::Cmd, path, "cmd", &[]); shell_path.map(|shell_path| Shell { shell_type: ShellType::Cmd,