diff --git a/codex-rs/core/src/conversation_manager.rs b/codex-rs/core/src/conversation_manager.rs index 3ca361a38a..5fb93ef06a 100644 --- a/codex-rs/core/src/conversation_manager.rs +++ b/codex-rs/core/src/conversation_manager.rs @@ -115,7 +115,8 @@ impl ConversationManager { rollout_path: PathBuf, auth_manager: Arc, ) -> CodexResult { - let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?; + let initial_history = + RolloutRecorder::get_rollout_history(&rollout_path, &config.cwd).await?; let CodexSpawnOk { codex, conversation_id, @@ -145,7 +146,7 @@ impl ConversationManager { path: PathBuf, ) -> CodexResult { // Compute the prefix up to the cut point. - let history = RolloutRecorder::get_rollout_history(&path).await?; + let history = RolloutRecorder::get_rollout_history(&path, &config.cwd).await?; let history = truncate_after_nth_user_message(history, nth_user_message); // Spawn a new conversation with the computed initial history. diff --git a/codex-rs/core/src/rollout/list.rs b/codex-rs/core/src/rollout/list.rs index 6f06709396..507012e2ea 100644 --- a/codex-rs/core/src/rollout/list.rs +++ b/codex-rs/core/src/rollout/list.rs @@ -17,6 +17,7 @@ use super::SESSIONS_SUBDIR; use crate::protocol::EventMsg; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; +use codex_protocol::protocol::SessionMetaLine; /// Returned page of conversation summaries. #[derive(Debug, Default, PartialEq)] @@ -87,6 +88,7 @@ impl<'de> serde::Deserialize<'de> for Cursor { /// concurrent new sessions being appended. Ordering is stable by timestamp desc, then UUID desc. pub(crate) async fn get_conversations( codex_home: &Path, + cwd: &Path, page_size: usize, cursor: Option<&Cursor>, ) -> io::Result { @@ -104,14 +106,26 @@ pub(crate) async fn get_conversations( let anchor = cursor.cloned(); - let result = traverse_directories_for_paths(root.clone(), page_size, anchor).await?; + let result = traverse_directories_for_paths(root.clone(), cwd, page_size, anchor).await?; Ok(result) } /// Load the full contents of a single conversation session file at `path`. /// Returns the entire file contents as a String. #[allow(dead_code)] -pub(crate) async fn get_conversation(path: &Path) -> io::Result { +pub(crate) async fn get_conversation(path: &Path, cwd: &Path) -> io::Result { + let (_head, session_meta, _, _) = read_head_and_flags(path, 1).await?; + let Some(meta) = session_meta else { + return Err(io::Error::other("missing session meta in rollout file")); + }; + if meta.meta.cwd != cwd { + return Err(io::Error::other(format!( + "session cwd `{}` does not match requested cwd `{}`", + meta.meta.cwd.display(), + cwd.display() + ))); + } + tokio::fs::read_to_string(path).await } @@ -121,6 +135,7 @@ pub(crate) async fn get_conversation(path: &Path) -> io::Result { /// Returned newest (latest) first. async fn traverse_directories_for_paths( root: PathBuf, + cwd: &Path, page_size: usize, anchor: Option, ) -> io::Result { @@ -176,12 +191,20 @@ async fn traverse_directories_for_paths( } // Read head and simultaneously detect message events within the same // first N JSONL records to avoid a second file read. - let (head, saw_session_meta, saw_user_event) = - read_head_and_flags(&path, HEAD_RECORD_LIMIT) - .await - .unwrap_or((Vec::new(), false, false)); - // Apply filters: must have session meta and at least one user message event - if saw_session_meta && saw_user_event { + let (head, session_meta, saw_session_meta, saw_user_event) = + match read_head_and_flags(&path, HEAD_RECORD_LIMIT).await { + Ok(res) => res, + Err(_) => continue, + }; + // Apply filters: must have session meta, matching cwd, and at least one + // user message event + if saw_session_meta + && saw_user_event + && session_meta + .as_ref() + .map(|meta| meta.meta.cwd.as_path() == cwd) + .unwrap_or(false) + { items.push(ConversationItem { path, head }); } } @@ -289,7 +312,7 @@ fn parse_timestamp_uuid_from_filename(name: &str) -> Option<(OffsetDateTime, Uui async fn read_head_and_flags( path: &Path, max_records: usize, -) -> io::Result<(Vec, bool, bool)> { +) -> io::Result<(Vec, Option, bool, bool)> { use tokio::io::AsyncBufReadExt; let file = tokio::fs::File::open(path).await?; @@ -298,6 +321,7 @@ async fn read_head_and_flags( let mut head: Vec = Vec::new(); let mut saw_session_meta = false; let mut saw_user_event = false; + let mut session_meta: Option = None; while head.len() < max_records { let line_opt = lines.next_line().await?; @@ -312,10 +336,13 @@ async fn read_head_and_flags( match rollout_line.item { RolloutItem::SessionMeta(session_meta_line) => { - if let Ok(val) = serde_json::to_value(session_meta_line) { + if let Ok(val) = serde_json::to_value(&session_meta_line) { head.push(val); saw_session_meta = true; } + if session_meta.is_none() { + session_meta = Some(session_meta_line); + } } RolloutItem::ResponseItem(item) => { if let Ok(val) = serde_json::to_value(item) { @@ -336,7 +363,7 @@ async fn read_head_and_flags( } } - Ok((head, saw_session_meta, saw_user_event)) + Ok((head, session_meta, saw_session_meta, saw_user_event)) } /// Locate a recorded conversation rollout file by its UUID string using the existing diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index 6befdb1fe9..25e00c3b5c 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -103,10 +103,11 @@ impl RolloutRecorder { /// List conversations (rollout files) under the provided Codex home directory. pub async fn list_conversations( codex_home: &Path, + cwd: &Path, page_size: usize, cursor: Option<&Cursor>, ) -> std::io::Result { - get_conversations(codex_home, page_size, cursor).await + get_conversations(codex_home, cwd, page_size, cursor).await } /// Attempt to create a new [`RolloutRecorder`]. If the sessions directory @@ -202,7 +203,10 @@ impl RolloutRecorder { .map_err(|e| IoError::other(format!("failed waiting for rollout flush: {e}"))) } - pub(crate) async fn get_rollout_history(path: &Path) -> std::io::Result { + pub(crate) async fn get_rollout_history( + path: &Path, + cwd: &Path, + ) -> std::io::Result { info!("Resuming rollout from {path:?}"); let text = tokio::fs::read_to_string(path).await?; if text.trim().is_empty() { @@ -211,6 +215,7 @@ impl RolloutRecorder { let mut items: Vec = Vec::new(); let mut conversation_id: Option = None; + let mut session_cwd: Option = None; for line in text.lines() { if line.trim().is_empty() { continue; @@ -232,6 +237,9 @@ impl RolloutRecorder { if conversation_id.is_none() { conversation_id = Some(session_meta_line.meta.id); } + if session_cwd.is_none() { + session_cwd = Some(session_meta_line.meta.cwd.clone()); + } items.push(RolloutItem::SessionMeta(session_meta_line)); } RolloutItem::ResponseItem(item) => { @@ -261,6 +269,22 @@ impl RolloutRecorder { let conversation_id = conversation_id .ok_or_else(|| IoError::other("failed to parse conversation ID from rollout file"))?; + match session_cwd { + Some(meta_cwd) if meta_cwd == cwd => {} + Some(meta_cwd) => { + return Err(IoError::other(format!( + "session cwd `{}` does not match requested cwd `{}`", + meta_cwd.display(), + cwd.display() + ))); + } + None => { + return Err(IoError::other( + "failed to parse session cwd from rollout file", + )); + } + } + if items.is_empty() { return Ok(InitialHistory::New); } diff --git a/codex-rs/core/src/rollout/tests.rs b/codex-rs/core/src/rollout/tests.rs index 86f47645f9..98baa0a056 100644 --- a/codex-rs/core/src/rollout/tests.rs +++ b/codex-rs/core/src/rollout/tests.rs @@ -17,12 +17,14 @@ use crate::rollout::list::ConversationsPage; use crate::rollout::list::Cursor; use crate::rollout::list::get_conversation; use crate::rollout::list::get_conversations; +use crate::rollout::recorder::RolloutRecorder; fn write_session_file( root: &Path, ts_str: &str, uuid: Uuid, num_records: usize, + cwd: &Path, ) -> std::io::Result<(OffsetDateTime, Uuid)> { let format: &[FormatItem] = format_description!("[year]-[month]-[day]T[hour]-[minute]-[second]"); @@ -40,6 +42,7 @@ fn write_session_file( let file_path = dir.join(filename); let mut file = File::create(file_path)?; + let cwd_str = cwd.to_string_lossy(); let meta = serde_json::json!({ "timestamp": ts_str, "type": "session_meta", @@ -47,7 +50,7 @@ fn write_session_file( "id": uuid, "timestamp": ts_str, "instructions": null, - "cwd": ".", + "cwd": cwd_str, "originator": "test_originator", "cli_version": "test_version" } @@ -86,12 +89,13 @@ async fn test_list_conversations_latest_first() { let u2 = Uuid::from_u128(2); let u3 = Uuid::from_u128(3); + let cwd = Path::new("."); // Create three sessions across three days - write_session_file(home, "2025-01-01T12-00-00", u1, 3).unwrap(); - write_session_file(home, "2025-01-02T12-00-00", u2, 3).unwrap(); - write_session_file(home, "2025-01-03T12-00-00", u3, 3).unwrap(); + write_session_file(home, "2025-01-01T12-00-00", u1, 3, cwd).unwrap(); + write_session_file(home, "2025-01-02T12-00-00", u2, 3, cwd).unwrap(); + write_session_file(home, "2025-01-03T12-00-00", u3, 3, cwd).unwrap(); - let page = get_conversations(home, 10, None).await.unwrap(); + let page = get_conversations(home, cwd, 10, None).await.unwrap(); // Build expected objects let p1 = home @@ -164,6 +168,46 @@ async fn test_list_conversations_latest_first() { assert_eq!(page, expected); } +#[tokio::test] +async fn test_list_conversations_filters_by_cwd() { + let temp = TempDir::new().unwrap(); + let home = temp.path(); + + let match_cwd = Path::new("/match"); + let other_cwd = Path::new("/other"); + + let id_match = Uuid::from_u128(11); + let id_other = Uuid::from_u128(22); + + write_session_file(home, "2025-02-01T00-00-00", id_match, 1, match_cwd).unwrap(); + write_session_file(home, "2025-02-02T00-00-00", id_other, 1, other_cwd).unwrap(); + + let page_match = get_conversations(home, match_cwd, 10, None).await.unwrap(); + assert_eq!(page_match.items.len(), 1); + let head_id = page_match.items[0] + .head + .first() + .and_then(|meta| meta.get("id")) + .and_then(|id| id.as_str()) + .expect("id str"); + assert_eq!(head_id, id_match.to_string()); + + let page_other = get_conversations(home, other_cwd, 10, None).await.unwrap(); + assert_eq!(page_other.items.len(), 1); + let other_head_id = page_other.items[0] + .head + .first() + .and_then(|meta| meta.get("id")) + .and_then(|id| id.as_str()) + .expect("id str"); + assert_eq!(other_head_id, id_other.to_string()); + + let none_page = get_conversations(home, Path::new("/missing"), 10, None) + .await + .unwrap(); + assert!(none_page.items.is_empty()); +} + #[tokio::test] async fn test_pagination_cursor() { let temp = TempDir::new().unwrap(); @@ -177,13 +221,15 @@ async fn test_pagination_cursor() { let u5 = Uuid::from_u128(55); // Oldest to newest - write_session_file(home, "2025-03-01T09-00-00", u1, 1).unwrap(); - write_session_file(home, "2025-03-02T09-00-00", u2, 1).unwrap(); - write_session_file(home, "2025-03-03T09-00-00", u3, 1).unwrap(); - write_session_file(home, "2025-03-04T09-00-00", u4, 1).unwrap(); - write_session_file(home, "2025-03-05T09-00-00", u5, 1).unwrap(); + write_session_file(home, "2025-03-01T09-00-00", u1, 1, Path::new(".")).unwrap(); + write_session_file(home, "2025-03-02T09-00-00", u2, 1, Path::new(".")).unwrap(); + write_session_file(home, "2025-03-03T09-00-00", u3, 1, Path::new(".")).unwrap(); + write_session_file(home, "2025-03-04T09-00-00", u4, 1, Path::new(".")).unwrap(); + write_session_file(home, "2025-03-05T09-00-00", u5, 1, Path::new(".")).unwrap(); - let page1 = get_conversations(home, 2, None).await.unwrap(); + let page1 = get_conversations(home, Path::new("."), 2, None) + .await + .unwrap(); let p5 = home .join("sessions") .join("2025") @@ -231,7 +277,7 @@ async fn test_pagination_cursor() { }; assert_eq!(page1, expected_page1); - let page2 = get_conversations(home, 2, page1.next_cursor.as_ref()) + let page2 = get_conversations(home, Path::new("."), 2, page1.next_cursor.as_ref()) .await .unwrap(); let p3 = home @@ -281,7 +327,7 @@ async fn test_pagination_cursor() { }; assert_eq!(page2, expected_page2); - let page3 = get_conversations(home, 2, page2.next_cursor.as_ref()) + let page3 = get_conversations(home, Path::new("."), 2, page2.next_cursor.as_ref()) .await .unwrap(); let p1 = home @@ -319,12 +365,14 @@ async fn test_get_conversation_contents() { let uuid = Uuid::new_v4(); let ts = "2025-04-01T10-30-00"; - write_session_file(home, ts, uuid, 2).unwrap(); + write_session_file(home, ts, uuid, 2, Path::new(".")).unwrap(); - let page = get_conversations(home, 1, None).await.unwrap(); + let page = get_conversations(home, Path::new("."), 1, None) + .await + .unwrap(); let path = &page.items[0].path; - let content = get_conversation(path).await.unwrap(); + let content = get_conversation(path, Path::new(".")).await.unwrap(); // Page equality (single item) let expected_path = home @@ -376,11 +424,13 @@ async fn test_stable_ordering_same_second_pagination() { let u2 = Uuid::from_u128(2); let u3 = Uuid::from_u128(3); - write_session_file(home, ts, u1, 0).unwrap(); - write_session_file(home, ts, u2, 0).unwrap(); - write_session_file(home, ts, u3, 0).unwrap(); + write_session_file(home, ts, u1, 0, Path::new(".")).unwrap(); + write_session_file(home, ts, u2, 0, Path::new(".")).unwrap(); + write_session_file(home, ts, u3, 0, Path::new(".")).unwrap(); - let page1 = get_conversations(home, 2, None).await.unwrap(); + let page1 = get_conversations(home, Path::new("."), 2, None) + .await + .unwrap(); let p3 = home .join("sessions") @@ -422,7 +472,7 @@ async fn test_stable_ordering_same_second_pagination() { }; assert_eq!(page1, expected_page1); - let page2 = get_conversations(home, 2, page1.next_cursor.as_ref()) + let page2 = get_conversations(home, Path::new("."), 2, page1.next_cursor.as_ref()) .await .unwrap(); let p1 = home @@ -443,3 +493,31 @@ async fn test_stable_ordering_same_second_pagination() { }; assert_eq!(page2, expected_page2); } + +#[tokio::test] +async fn test_get_rollout_history_enforces_cwd() { + let temp = TempDir::new().unwrap(); + let home = temp.path(); + + let ts = "2025-08-01T00-00-00"; + let uuid = Uuid::new_v4(); + let match_cwd = Path::new("/history-match"); + + write_session_file(home, ts, uuid, 1, match_cwd).unwrap(); + + let path = home + .join("sessions") + .join("2025") + .join("08") + .join("01") + .join(format!("rollout-{ts}-{uuid}.jsonl")); + + RolloutRecorder::get_rollout_history(&path, match_cwd) + .await + .expect("matching cwd should succeed"); + + let err = RolloutRecorder::get_rollout_history(&path, Path::new("/history-other")) + .await + .expect_err("mismatched cwd should error"); + assert!(err.to_string().contains("does not match")); +} diff --git a/codex-rs/core/tests/suite/cli_stream.rs b/codex-rs/core/tests/suite/cli_stream.rs index 1a0c016272..6a1c2ce225 100644 --- a/codex-rs/core/tests/suite/cli_stream.rs +++ b/codex-rs/core/tests/suite/cli_stream.rs @@ -2,6 +2,7 @@ use assert_cmd::Command as AssertCommand; use codex_core::RolloutRecorder; use codex_core::protocol::GitInfo; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; +use std::path::Path; use std::time::Duration; use std::time::Instant; use tempfile::TempDir; @@ -81,7 +82,8 @@ async fn chat_mode_stream_cli() { server.verify().await; // Verify a new session rollout was created and is discoverable via list_conversations - let page = RolloutRecorder::list_conversations(home.path(), 10, None) + let cwd = Path::new(env!("CARGO_MANIFEST_DIR")); + let page = RolloutRecorder::list_conversations(home.path(), cwd, 10, None) .await .expect("list conversations"); assert!( diff --git a/codex-rs/core/tests/suite/rollout_list_find.rs b/codex-rs/core/tests/suite/rollout_list_find.rs index 88409a4616..1b8800f416 100644 --- a/codex-rs/core/tests/suite/rollout_list_find.rs +++ b/codex-rs/core/tests/suite/rollout_list_find.rs @@ -8,12 +8,13 @@ use uuid::Uuid; /// Create sessions/YYYY/MM/DD and write a minimal rollout file containing the /// provided conversation id in the SessionMeta line. Returns the absolute path. -fn write_minimal_rollout_with_id(codex_home: &TempDir, id: Uuid) -> PathBuf { +fn write_minimal_rollout_with_id(codex_home: &TempDir, id: Uuid, cwd: &std::path::Path) -> PathBuf { let sessions = codex_home.path().join("sessions/2024/01/01"); std::fs::create_dir_all(&sessions).unwrap(); let file = sessions.join(format!("rollout-2024-01-01T00-00-00-{id}.jsonl")); let mut f = std::fs::File::create(&file).unwrap(); + let cwd_str = cwd.to_string_lossy(); // Minimal first line: session_meta with the id so content search can find it writeln!( f, @@ -25,7 +26,7 @@ fn write_minimal_rollout_with_id(codex_home: &TempDir, id: Uuid) -> PathBuf { "id": id, "timestamp": "2024-01-01T00:00:00Z", "instructions": null, - "cwd": ".", + "cwd": cwd_str, "originator": "test", "cli_version": "test" } @@ -40,7 +41,8 @@ fn write_minimal_rollout_with_id(codex_home: &TempDir, id: Uuid) -> PathBuf { async fn find_locates_rollout_file_by_id() { let home = TempDir::new().unwrap(); let id = Uuid::new_v4(); - let expected = write_minimal_rollout_with_id(&home, id); + let cwd = std::path::Path::new("."); + let expected = write_minimal_rollout_with_id(&home, id, cwd); let found = find_conversation_path_by_id_str(home.path(), &id.to_string()) .await diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 097231da60..9f1761d67f 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -313,7 +313,14 @@ async fn resolve_resume_path( args: &crate::cli::ResumeArgs, ) -> anyhow::Result> { if args.last { - match codex_core::RolloutRecorder::list_conversations(&config.codex_home, 1, None).await { + match codex_core::RolloutRecorder::list_conversations( + &config.codex_home, + &config.cwd, + 1, + None, + ) + .await + { Ok(page) => Ok(page.items.first().map(|it| it.path.clone())), Err(e) => { error!("Error listing conversations: {e}"); diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/mcp-server/src/codex_message_processor.rs index ceb47889be..9c3e15e2c1 100644 --- a/codex-rs/mcp-server/src/codex_message_processor.rs +++ b/codex-rs/mcp-server/src/codex_message_processor.rs @@ -675,6 +675,7 @@ impl CodexMessageProcessor { let page = match RolloutRecorder::list_conversations( &self.config.codex_home, + &self.config.cwd, page_size, cursor_ref, ) diff --git a/codex-rs/mcp-server/tests/suite/list_resume.rs b/codex-rs/mcp-server/tests/suite/list_resume.rs index 9b5748cab5..1d55d788c2 100644 --- a/codex-rs/mcp-server/tests/suite/list_resume.rs +++ b/codex-rs/mcp-server/tests/suite/list_resume.rs @@ -23,23 +23,27 @@ const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs async fn test_list_and_resume_conversations() { // Prepare a temporary CODEX_HOME with a few fake rollout files. let codex_home = TempDir::new().expect("create temp dir"); + let cwd = std::env::current_dir().expect("current dir"); create_fake_rollout( codex_home.path(), "2025-01-02T12-00-00", "2025-01-02T12:00:00Z", "Hello A", + &cwd, ); create_fake_rollout( codex_home.path(), "2025-01-01T13-00-00", "2025-01-01T13:00:00Z", "Hello B", + &cwd, ); create_fake_rollout( codex_home.path(), "2025-01-01T12-00-00", "2025-01-01T12:00:00Z", "Hello C", + &cwd, ); let mut mcp = McpProcess::new(codex_home.path()) @@ -145,7 +149,13 @@ async fn test_list_and_resume_conversations() { let _: uuid::Uuid = conversation_id.into(); } -fn create_fake_rollout(codex_home: &Path, filename_ts: &str, meta_rfc3339: &str, preview: &str) { +fn create_fake_rollout( + codex_home: &Path, + filename_ts: &str, + meta_rfc3339: &str, + preview: &str, + cwd: &Path, +) { let uuid = Uuid::new_v4(); // sessions/YYYY/MM/DD/ derived from filename_ts (YYYY-MM-DDThh-mm-ss) let year = &filename_ts[0..4]; @@ -156,6 +166,7 @@ fn create_fake_rollout(codex_home: &Path, filename_ts: &str, meta_rfc3339: &str, let file_path = dir.join(format!("rollout-{filename_ts}-{uuid}.jsonl")); let mut lines = Vec::new(); + let cwd_str = cwd.to_string_lossy(); // Meta line with timestamp (flattened meta in payload for new schema) lines.push( json!({ @@ -164,7 +175,7 @@ fn create_fake_rollout(codex_home: &Path, filename_ts: &str, meta_rfc3339: &str, "payload": { "id": uuid, "timestamp": meta_rfc3339, - "cwd": "/", + "cwd": cwd_str, "originator": "codex", "cli_version": "0.0.0", "instructions": null diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 7f97b3333f..245cd07c28 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -354,7 +354,7 @@ async fn run_ratatui_app( } } } else if cli.resume_last { - match RolloutRecorder::list_conversations(&config.codex_home, 1, None).await { + match RolloutRecorder::list_conversations(&config.codex_home, &config.cwd, 1, None).await { Ok(page) => page .items .first() @@ -363,7 +363,7 @@ async fn run_ratatui_app( Err(_) => resume_picker::ResumeSelection::StartFresh, } } else if cli.resume_picker { - match resume_picker::run_resume_picker(&mut tui, &config.codex_home).await? { + match resume_picker::run_resume_picker(&mut tui, &config.codex_home, &config.cwd).await? { resume_picker::ResumeSelection::Exit => { restore(); session_log::log_session_end(); diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 8609ab0c4d..639c3e07b7 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -39,9 +39,17 @@ pub enum ResumeSelection { /// Interactive session picker that lists recorded rollout files with simple /// search and pagination. Shows the first user input as the preview, relative /// time (e.g., "5 seconds ago"), and the absolute path. -pub async fn run_resume_picker(tui: &mut Tui, codex_home: &Path) -> Result { +pub async fn run_resume_picker( + tui: &mut Tui, + codex_home: &Path, + cwd: &Path, +) -> Result { let alt = AltScreenGuard::enter(tui); - let mut state = PickerState::new(codex_home.to_path_buf(), alt.tui.frame_requester()); + let mut state = PickerState::new( + codex_home.to_path_buf(), + cwd.to_path_buf(), + alt.tui.frame_requester(), + ); state.load_page(None).await?; state.request_frame(); @@ -88,6 +96,7 @@ impl Drop for AltScreenGuard<'_> { struct PickerState { codex_home: PathBuf, + cwd: PathBuf, requester: FrameRequester, // pagination pagination: Pagination, @@ -115,9 +124,10 @@ struct Row { } impl PickerState { - fn new(codex_home: PathBuf, requester: FrameRequester) -> Self { + fn new(codex_home: PathBuf, cwd: PathBuf, requester: FrameRequester) -> Self { Self { codex_home, + cwd, requester, pagination: Pagination { current_anchor: None, @@ -225,7 +235,9 @@ impl PickerState { } async fn load_page(&mut self, anchor: Option<&Cursor>) -> Result<()> { - let page = RolloutRecorder::list_conversations(&self.codex_home, PAGE_SIZE, anchor).await?; + let page = + RolloutRecorder::list_conversations(&self.codex_home, &self.cwd, PAGE_SIZE, anchor) + .await?; self.pagination.next_cursor = page.next_cursor.clone(); self.all_rows = to_rows(page); self.apply_filter();