diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index dfafb2911..7151f95b9 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -28,6 +28,7 @@ use super::policy::is_persisted_response_item; use crate::config::Config; use crate::default_client::originator; use crate::git_info::collect_git_info; +use crate::path_utils; use codex_protocol::protocol::InitialHistory; use codex_protocol::protocol::ResumedHistory; use codex_protocol::protocol::RolloutItem; @@ -113,6 +114,35 @@ impl RolloutRecorder { .await } + /// Find the newest recorded thread path, optionally filtering to a matching cwd. + pub async fn find_latest_thread_path( + codex_home: &Path, + allowed_sources: &[SessionSource], + model_providers: Option<&[String]>, + default_provider: &str, + filter_cwd: Option<&Path>, + ) -> std::io::Result> { + let mut cursor: Option = None; + loop { + let page = Self::list_threads( + codex_home, + 25, + cursor.as_ref(), + allowed_sources, + model_providers, + default_provider, + ) + .await?; + if let Some(path) = select_resume_path(&page, filter_cwd) { + return Ok(Some(path)); + } + cursor = page.next_cursor; + if cursor.is_none() { + return Ok(None); + } + } + } + /// Attempt to create a new [`RolloutRecorder`]. If the sessions directory /// cannot be created or the rollout file cannot be opened we return the /// error so the caller can decide whether to disable persistence. @@ -431,3 +461,36 @@ impl JsonlWriter { Ok(()) } } + +fn select_resume_path(page: &ThreadsPage, filter_cwd: Option<&Path>) -> Option { + match filter_cwd { + Some(cwd) => page.items.iter().find_map(|item| { + if session_cwd_matches(&item.head, cwd) { + Some(item.path.clone()) + } else { + None + } + }), + None => page.items.first().map(|item| item.path.clone()), + } +} + +fn session_cwd_matches(head: &[serde_json::Value], cwd: &Path) -> bool { + let Some(session_cwd) = extract_session_cwd(head) else { + return false; + }; + if let (Ok(ca), Ok(cb)) = ( + path_utils::normalize_for_path_comparison(&session_cwd), + path_utils::normalize_for_path_comparison(cwd), + ) { + return ca == cb; + } + session_cwd == cwd +} + +fn extract_session_cwd(head: &[serde_json::Value]) -> Option { + head.iter().find_map(|value| { + let meta_line = serde_json::from_value::(value.clone()).ok()?; + Some(meta_line.meta.cwd) + }) +} diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index a7bab5c65..29bf2b1ce 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -522,22 +522,22 @@ async fn run_ratatui_app( } } else if cli.resume_last { let provider_filter = vec![config.model_provider_id.clone()]; - match RolloutRecorder::list_threads( + let filter_cwd = if cli.resume_show_all { + None + } else { + Some(config.cwd.as_path()) + }; + match RolloutRecorder::find_latest_thread_path( &config.codex_home, - 1, - None, INTERACTIVE_SESSION_SOURCES, Some(provider_filter.as_slice()), &config.model_provider_id, + filter_cwd, ) .await { - Ok(page) => page - .items - .first() - .map(|it| resume_picker::SessionSelection::Resume(it.path.clone())) - .unwrap_or(resume_picker::SessionSelection::StartFresh), - Err(_) => resume_picker::SessionSelection::StartFresh, + Ok(Some(path)) => resume_picker::SessionSelection::Resume(path), + _ => resume_picker::SessionSelection::StartFresh, } } else if cli.resume_picker { match resume_picker::run_resume_picker( diff --git a/codex-rs/tui2/src/lib.rs b/codex-rs/tui2/src/lib.rs index 9ababcab3..55e640c58 100644 --- a/codex-rs/tui2/src/lib.rs +++ b/codex-rs/tui2/src/lib.rs @@ -545,22 +545,22 @@ async fn run_ratatui_app( } } else if cli.resume_last { let provider_filter = vec![config.model_provider_id.clone()]; - match RolloutRecorder::list_threads( + let filter_cwd = if cli.resume_show_all { + None + } else { + Some(config.cwd.as_path()) + }; + match RolloutRecorder::find_latest_thread_path( &config.codex_home, - 1, - None, INTERACTIVE_SESSION_SOURCES, Some(provider_filter.as_slice()), &config.model_provider_id, + filter_cwd, ) .await { - Ok(page) => page - .items - .first() - .map(|it| resume_picker::SessionSelection::Resume(it.path.clone())) - .unwrap_or(resume_picker::SessionSelection::StartFresh), - Err(_) => resume_picker::SessionSelection::StartFresh, + Ok(Some(path)) => resume_picker::SessionSelection::Resume(path), + _ => resume_picker::SessionSelection::StartFresh, } } else if cli.resume_picker { match resume_picker::run_resume_picker(