From 027e5d9358a106007e9d3f982a58768d25214433 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 25 Nov 2025 10:50:15 -0800 Subject: [PATCH 1/8] add slash resume --- codex-rs/tui/src/app.rs | 67 ++++++++++++++++++++++++++++ codex-rs/tui/src/app_event.rs | 3 ++ codex-rs/tui/src/chatwidget.rs | 3 ++ codex-rs/tui/src/chatwidget/tests.rs | 9 ++++ codex-rs/tui/src/resume_picker.rs | 58 ++++++++++++++++++------ codex-rs/tui/src/slash_command.rs | 3 ++ docs/slash_commands.md | 1 + 7 files changed, 130 insertions(+), 14 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index d0e057102c..4ff5cd790d 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -468,6 +468,73 @@ impl App { } tui.frame_requester().schedule_frame(); } + AppEvent::OpenResumePicker => { + match crate::resume_picker::run_resume_picker( + tui, + &self.config.codex_home, + &self.config.model_provider_id, + false, + ) + .await? + { + ResumeSelection::Resume(path) => { + let summary = session_summary( + self.chat_widget.token_usage(), + self.chat_widget.conversation_id(), + ); + match self + .server + .resume_conversation_from_rollout( + self.config.clone(), + path.clone(), + self.auth_manager.clone(), + ) + .await + { + Ok(resumed) => { + self.shutdown_current_conversation().await; + let init = crate::chatwidget::ChatWidgetInit { + config: self.config.clone(), + frame_requester: tui.frame_requester(), + app_event_tx: self.app_event_tx.clone(), + initial_prompt: None, + initial_images: Vec::new(), + enhanced_keys_supported: self.enhanced_keys_supported, + auth_manager: self.auth_manager.clone(), + feedback: self.feedback.clone(), + }; + self.chat_widget = ChatWidget::new_from_existing( + init, + resumed.conversation, + resumed.session_configured, + ); + if let Some(summary) = summary { + let mut lines: Vec> = + vec![summary.usage_line.clone().into()]; + if let Some(command) = summary.resume_command { + let spans = vec![ + "To continue this session, run ".into(), + command.cyan(), + ]; + lines.push(spans.into()); + } + self.chat_widget.add_plain_history_lines(lines); + } + } + Err(err) => { + self.chat_widget.add_error_message(format!( + "Failed to resume session from {}: {err}", + path.display() + )); + } + } + } + ResumeSelection::Exit | ResumeSelection::StartFresh => {} + } + + // Leaving alt-screen may blank the inline viewport; force a redraw either way. + tui.frame_requester().schedule_frame(); + } AppEvent::InsertHistoryCell(cell) => { let cell: Arc = cell.into(); if let Some(Overlay::Transcript(t)) = &mut self.overlay { diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index cf494f57d6..944eeda810 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -22,6 +22,9 @@ pub(crate) enum AppEvent { /// Start a new session. NewSession, + /// Open the resume picker inside the running TUI session. + OpenResumePicker, + /// Request to exit the application gracefully. ExitRequest, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 53c77f82be..b9d990a42c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1454,6 +1454,9 @@ impl ChatWidget { SlashCommand::New => { self.app_event_tx.send(AppEvent::NewSession); } + SlashCommand::Resume => { + self.app_event_tx.send(AppEvent::OpenResumePicker); + } SlashCommand::Init => { let init_target = self.config.cwd.join(DEFAULT_PROJECT_DOC_FILENAME); if init_target.exists() { diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 7b044e8dfb..050bf913e8 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1096,6 +1096,15 @@ fn slash_exit_requests_exit() { assert_matches!(rx.try_recv(), Ok(AppEvent::ExitRequest)); } +#[test] +fn slash_resume_opens_picker() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); + + chat.dispatch_command(SlashCommand::Resume); + + assert_matches!(rx.try_recv(), Ok(AppEvent::OpenResumePicker)); +} + #[test] fn slash_undo_sends_op() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 6a21496d34..abbefed3cf 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -21,9 +21,11 @@ use ratatui::layout::Rect; use ratatui::style::Stylize as _; use ratatui::text::Line; use ratatui::text::Span; +use std::time::Instant; use tokio::sync::mpsc; use tokio_stream::StreamExt; use tokio_stream::wrappers::UnboundedReceiverStream; +use tracing::error; use unicode_width::UnicodeWidthStr; use crate::diff_render::display_path_for; @@ -87,6 +89,7 @@ pub async fn run_resume_picker( let page_loader: PageLoader = Arc::new(move |request: PageLoadRequest| { let tx = loader_tx.clone(); tokio::spawn(async move { + let started = Instant::now(); let provider_filter = vec![request.default_provider.clone()]; let page = RolloutRecorder::list_conversations( &request.codex_home, @@ -97,6 +100,30 @@ pub async fn run_resume_picker( request.default_provider.as_str(), ) .await; + match &page { + Ok(p) => { + error!( + elapsed_ms = started.elapsed().as_millis(), + request_token = request.request_token, + search_token = request.search_token, + scanned = p.num_scanned_files, + returned = p.items.len(), + reached_scan_cap = p.reached_scan_cap, + cursor = ?request.cursor, + "resume_picker list_conversations page", + ); + } + Err(e) => { + error!( + elapsed_ms = started.elapsed().as_millis(), + request_token = request.request_token, + search_token = request.search_token, + cursor = ?request.cursor, + error = %e, + "resume_picker list_conversations error", + ); + } + } let _ = tx.send(BackgroundEvent::PageLoaded { request_token: request.request_token, search_token: request.search_token, @@ -113,7 +140,7 @@ pub async fn run_resume_picker( show_all, filter_cwd, ); - state.load_initial_page().await?; + state.start_initial_load(); state.request_frame(); let mut tui_events = alt.tui.event_stream().fuse(); @@ -359,25 +386,28 @@ impl PickerState { Ok(None) } - async fn load_initial_page(&mut self) -> Result<()> { - let provider_filter = vec![self.default_provider.clone()]; - let page = RolloutRecorder::list_conversations( - &self.codex_home, - PAGE_SIZE, - None, - INTERACTIVE_SESSION_SOURCES, - Some(provider_filter.as_slice()), - self.default_provider.as_str(), - ) - .await?; + fn start_initial_load(&mut self) { self.reset_pagination(); self.all_rows.clear(); self.filtered_rows.clear(); self.seen_paths.clear(); self.search_state = SearchState::Idle; self.selected = 0; - self.ingest_page(page); - Ok(()) + + let request_token = self.allocate_request_token(); + self.pagination.loading = LoadingState::Pending(PendingLoad { + request_token, + search_token: None, + }); + self.request_frame(); + + (self.page_loader)(PageLoadRequest { + codex_home: self.codex_home.clone(), + cursor: None, + request_token, + search_token: None, + default_provider: self.default_provider.clone(), + }); } fn handle_background_event(&mut self, event: BackgroundEvent) -> Result<()> { diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 969d279b07..47b330cba6 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -16,6 +16,7 @@ pub enum SlashCommand { Approvals, Review, New, + Resume, Init, Compact, Undo, @@ -40,6 +41,7 @@ impl SlashCommand { SlashCommand::Init => "create an AGENTS.md file with instructions for Codex", SlashCommand::Compact => "summarize conversation to prevent hitting the context limit", SlashCommand::Review => "review my current changes and find issues", + SlashCommand::Resume => "resume a saved chat", SlashCommand::Undo => "ask Codex to undo a turn", SlashCommand::Quit | SlashCommand::Exit => "exit Codex", SlashCommand::Diff => "show git diff (including untracked files)", @@ -64,6 +66,7 @@ impl SlashCommand { pub fn available_during_task(self) -> bool { match self { SlashCommand::New + | SlashCommand::Resume | SlashCommand::Init | SlashCommand::Compact | SlashCommand::Undo diff --git a/docs/slash_commands.md b/docs/slash_commands.md index 4c1a244764..35cfc52b09 100644 --- a/docs/slash_commands.md +++ b/docs/slash_commands.md @@ -16,6 +16,7 @@ Control Codex’s behavior during an interactive session with slash commands. | `/approvals` | choose what Codex can do without approval | | `/review` | review my current changes and find issues | | `/new` | start a new chat during a conversation | +| `/resume` | resume a saved chat | | `/init` | create an AGENTS.md file with instructions for Codex | | `/compact` | summarize conversation to prevent hitting the context limit | | `/undo` | ask Codex to undo a turn | From b3770aa8501b5cc8b791ac0359e984af9bd894b5 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 25 Nov 2025 10:56:20 -0800 Subject: [PATCH 2/8] add slash resume --- codex-rs/tui/src/resume_picker.rs | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index abbefed3cf..0835fa94e0 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -21,11 +21,9 @@ use ratatui::layout::Rect; use ratatui::style::Stylize as _; use ratatui::text::Line; use ratatui::text::Span; -use std::time::Instant; use tokio::sync::mpsc; use tokio_stream::StreamExt; use tokio_stream::wrappers::UnboundedReceiverStream; -use tracing::error; use unicode_width::UnicodeWidthStr; use crate::diff_render::display_path_for; @@ -89,7 +87,6 @@ pub async fn run_resume_picker( let page_loader: PageLoader = Arc::new(move |request: PageLoadRequest| { let tx = loader_tx.clone(); tokio::spawn(async move { - let started = Instant::now(); let provider_filter = vec![request.default_provider.clone()]; let page = RolloutRecorder::list_conversations( &request.codex_home, @@ -100,30 +97,6 @@ pub async fn run_resume_picker( request.default_provider.as_str(), ) .await; - match &page { - Ok(p) => { - error!( - elapsed_ms = started.elapsed().as_millis(), - request_token = request.request_token, - search_token = request.search_token, - scanned = p.num_scanned_files, - returned = p.items.len(), - reached_scan_cap = p.reached_scan_cap, - cursor = ?request.cursor, - "resume_picker list_conversations page", - ); - } - Err(e) => { - error!( - elapsed_ms = started.elapsed().as_millis(), - request_token = request.request_token, - search_token = request.search_token, - cursor = ?request.cursor, - error = %e, - "resume_picker list_conversations error", - ); - } - } let _ = tx.send(BackgroundEvent::PageLoaded { request_token: request.request_token, search_token: request.search_token, From 851b46e84278eeaf2f2dc6ff0aad44c897bcafde Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 25 Nov 2025 11:02:38 -0800 Subject: [PATCH 3/8] tests --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 56 ++++++++++++ ...chat_composer__tests__slash_popup_res.snap | 11 +++ codex-rs/tui/src/resume_picker.rs | 90 +++++++++++++++++++ ...e_picker__tests__resume_picker_screen.snap | 14 +++ 4 files changed, 171 insertions(+) create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_res.snap create mode 100644 codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_screen.snap diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 6b42e5134c..a000e9c613 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2357,6 +2357,62 @@ mod tests { } } + #[test] + fn slash_popup_resume_for_res_ui() { + use ratatui::Terminal; + use ratatui::backend::TestBackend; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + + // Type "/res" humanlike so paste-burst doesn’t interfere. + type_chars_humanlike(&mut composer, &['/', 'r', 'e', 's']); + + let mut terminal = Terminal::new(TestBackend::new(60, 6)).expect("terminal"); + terminal + .draw(|f| composer.render(f.area(), f.buffer_mut())) + .expect("draw composer"); + + // Snapshot should show /resume as the first entry for /res. + insta::assert_snapshot!("slash_popup_res", terminal.backend()); + } + + #[test] + fn slash_popup_resume_for_res_logic() { + use super::super::command_popup::CommandItem; + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + type_chars_humanlike(&mut composer, &['/', 'r', 'e', 's']); + + match &composer.active_popup { + ActivePopup::Command(popup) => match popup.selected_item() { + Some(CommandItem::Builtin(cmd)) => { + assert_eq!(cmd.command(), "resume") + } + Some(CommandItem::UserPrompt(_)) => { + panic!("unexpected prompt selected for '/res'") + } + None => panic!("no selected command for '/res'"), + }, + _ => panic!("slash popup not active after typing '/res'"), + } + } + // Test helper: simulate human typing with a brief delay and flush the paste-burst buffer fn type_chars_humanlike(composer: &mut ChatComposer, chars: &[char]) { use crossterm::event::KeyCode; diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_res.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_res.snap new file mode 100644 index 0000000000..df8ea36e63 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_res.snap @@ -0,0 +1,11 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 2385 +expression: terminal.backend() +--- +" " +"› /res " +" " +" " +" " +" /resume resume a saved chat " diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 0835fa94e0..6e87120786 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -1278,6 +1278,96 @@ mod tests { assert_snapshot!("resume_picker_table", snapshot); } + #[test] + fn resume_picker_screen_snapshot() { + use crate::custom_terminal::Terminal; + use crate::test_backend::VT100Backend; + + let loader: PageLoader = Arc::new(|_| {}); + let mut state = PickerState::new( + PathBuf::from("/tmp"), + FrameRequester::test_dummy(), + loader, + String::from("openai"), + true, + None, + ); + + let now = Utc::now(); + let rows = vec![ + Row { + path: PathBuf::from("/tmp/a.jsonl"), + preview: String::from("Fix resume picker timestamps"), + created_at: Some(now - Duration::minutes(16)), + updated_at: Some(now - Duration::seconds(42)), + cwd: Some(PathBuf::from("/tmp/project")), + git_branch: Some(String::from("feature/resume")), + }, + Row { + path: PathBuf::from("/tmp/b.jsonl"), + preview: String::from("Investigate lazy pagination cap"), + created_at: Some(now - Duration::hours(1)), + updated_at: Some(now - Duration::minutes(35)), + cwd: Some(PathBuf::from("/tmp/other")), + git_branch: Some(String::from("main")), + }, + ]; + state.all_rows = rows.clone(); + state.filtered_rows = rows; + state.view_rows = Some(4); + state.selected = 0; + state.scroll_top = 0; + state.update_view_rows(4); + + let metrics = calculate_column_metrics(&state.filtered_rows, state.show_all); + + let width: u16 = 80; + let height: u16 = 9; + let backend = VT100Backend::new(width, height); + let mut terminal = Terminal::with_options(backend).expect("terminal"); + terminal.set_viewport_area(Rect::new(0, 0, width, height)); + + { + let mut frame = terminal.get_frame(); + let area = frame.area(); + let [header, search, columns, list, hint] = Layout::vertical([ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Min(area.height.saturating_sub(4)), + Constraint::Length(1), + ]) + .areas(area); + + frame.render_widget_ref( + Line::from(vec!["Resume a previous session".bold().cyan()]), + header, + ); + + frame.render_widget_ref(Line::from("Type to search".dim()), search); + + render_column_headers(&mut frame, columns, &metrics); + render_list(&mut frame, list, &state, &metrics); + + let hint_line: Line = vec![ + key_hint::plain(KeyCode::Enter).into(), + " to resume ".dim(), + " ".dim(), + key_hint::plain(KeyCode::Esc).into(), + " to start new ".dim(), + " ".dim(), + key_hint::ctrl(KeyCode::Char('c')).into(), + " to quit ".dim(), + ] + .into(); + frame.render_widget_ref(hint_line, hint); + } + terminal.flush().expect("flush"); + + let snapshot = terminal.backend().to_string(); + assert_snapshot!("resume_picker_screen", snapshot); + } + #[test] fn pageless_scrolling_deduplicates_and_keeps_order() { let loader: PageLoader = Arc::new(|_| {}); diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_screen.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_screen.snap new file mode 100644 index 0000000000..d9d5b3ec11 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_screen.snap @@ -0,0 +1,14 @@ +--- +source: tui/src/resume_picker.rs +assertion_line: 1368 +expression: snapshot +--- +Resume a previous session +Type to search + Updated Branch CWD Conversation +> 42 seconds ago feature/resume /tmp/project Fix resume picker timestamps + 35 minutes ago main /tmp/other Investigate lazy pagination cap + + + +enter to resume esc to start new ctrl + c to quit From a4657867205b891523713e3c4288fc80768d1509 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 25 Nov 2025 11:11:33 -0800 Subject: [PATCH 4/8] tests --- codex-rs/Cargo.lock | 1 + codex-rs/tui/Cargo.toml | 1 + codex-rs/tui/src/resume_picker.rs | 110 ++++++++++++++---- ...e_picker__tests__resume_picker_screen.snap | 8 +- 4 files changed, 96 insertions(+), 24 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 34ea8c8773..a99f380573 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1620,6 +1620,7 @@ dependencies = [ "unicode-segmentation", "unicode-width 0.2.1", "url", + "uuid", "vt100", ] diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index d9906b2f01..a084652fa1 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -109,3 +109,4 @@ pretty_assertions = { workspace = true } rand = { workspace = true } serial_test = { workspace = true } vt100 = { workspace = true } +uuid = { workspace = true } diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 6e87120786..38295f8abd 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -1282,10 +1282,88 @@ mod tests { fn resume_picker_screen_snapshot() { use crate::custom_terminal::Terminal; use crate::test_backend::VT100Backend; + use uuid::Uuid; + + // Create real rollout files so the snapshot uses the actual listing pipeline. + let tempdir = tempfile::tempdir().expect("tempdir"); + let sessions_root = tempdir.path().join("sessions"); + std::fs::create_dir_all(&sessions_root).expect("mkdir sessions root"); + + let now = Utc::now(); + + // Helper to write a rollout file with minimal meta + one user message. + let write_rollout = |ts: DateTime, cwd: &str, branch: &str, preview: &str| { + let dir = sessions_root + .join(ts.format("%Y").to_string()) + .join(ts.format("%m").to_string()) + .join(ts.format("%d").to_string()); + std::fs::create_dir_all(&dir).expect("mkdir date dirs"); + let filename = format!( + "rollout-{}-{}.jsonl", + ts.format("%Y-%m-%dT%H-%M-%S"), + Uuid::new_v4() + ); + let path = dir.join(filename); + let meta = serde_json::json!({ + "timestamp": ts.to_rfc3339(), + "item": { + "SessionMeta": { + "meta": { + "id": Uuid::new_v4(), + "timestamp": ts.to_rfc3339(), + "cwd": cwd, + "originator": "user", + "cli_version": "0.0.0", + "instructions": null, + "source": "Cli", + "model_provider": "openai", + } + } + } + }); + let user = serde_json::json!({ + "timestamp": ts.to_rfc3339(), + "item": { + "EventMsg": { + "UserMessage": { + "message": preview, + "images": null + } + } + } + }); + let branch_meta = serde_json::json!({ + "timestamp": ts.to_rfc3339(), + "item": { + "EventMsg": { + "SessionMeta": { + "meta": { + "git_branch": branch + } + } + } + } + }); + std::fs::write(&path, format!("{}\n{}\n{}\n", meta, user, branch_meta)) + .expect("write rollout"); + }; + + write_rollout( + now - Duration::seconds(42), + "/tmp/project", + "feature/resume", + "Fix resume picker timestamps", + ); + write_rollout( + now - Duration::minutes(35), + "/tmp/other", + "main", + "Investigate lazy pagination cap", + ); let loader: PageLoader = Arc::new(|_| {}); let mut state = PickerState::new( - PathBuf::from("/tmp"), + sessions_root.clone(), FrameRequester::test_dummy(), loader, String::from("openai"), @@ -1293,25 +1371,17 @@ mod tests { None, ); - let now = Utc::now(); - let rows = vec![ - Row { - path: PathBuf::from("/tmp/a.jsonl"), - preview: String::from("Fix resume picker timestamps"), - created_at: Some(now - Duration::minutes(16)), - updated_at: Some(now - Duration::seconds(42)), - cwd: Some(PathBuf::from("/tmp/project")), - git_branch: Some(String::from("feature/resume")), - }, - Row { - path: PathBuf::from("/tmp/b.jsonl"), - preview: String::from("Investigate lazy pagination cap"), - created_at: Some(now - Duration::hours(1)), - updated_at: Some(now - Duration::minutes(35)), - cwd: Some(PathBuf::from("/tmp/other")), - git_branch: Some(String::from("main")), - }, - ]; + let page = block_on_future(RolloutRecorder::list_conversations( + &sessions_root, + PAGE_SIZE, + None, + INTERACTIVE_SESSION_SOURCES, + Some(&[String::from("openai")]), + "openai", + )) + .expect("list conversations"); + + let rows = rows_from_items(page.items); state.all_rows = rows.clone(); state.filtered_rows = rows; state.view_rows = Some(4); diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_screen.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_screen.snap index d9d5b3ec11..79a169a06d 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_screen.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_screen.snap @@ -1,13 +1,13 @@ --- source: tui/src/resume_picker.rs -assertion_line: 1368 +assertion_line: 1438 expression: snapshot --- Resume a previous session Type to search - Updated Branch CWD Conversation -> 42 seconds ago feature/resume /tmp/project Fix resume picker timestamps - 35 minutes ago main /tmp/other Investigate lazy pagination cap + Updated Branch CWD Conversation +No sessions yet + From 0e0da17c8a0bbc5faee96782c39275c227825549 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 25 Nov 2025 11:45:23 -0800 Subject: [PATCH 5/8] lint --- codex-rs/tui/src/resume_picker.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 38295f8abd..27932bca0a 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -1344,7 +1344,7 @@ mod tests { } } }); - std::fs::write(&path, format!("{}\n{}\n{}\n", meta, user, branch_meta)) + std::fs::write(&path, format!("{meta}\n{user}\n{branch_meta}\n")) .expect("write rollout"); }; From a91f878b9eb444ef8aef3626ad9ac066495572c4 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 2 Dec 2025 11:14:58 -0800 Subject: [PATCH 6/8] tmp --- codex-rs/tui/src/resume_picker.rs | 4 ++-- docs/slash_commands.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 27932bca0a..8bc1ec851d 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -1363,7 +1363,7 @@ mod tests { let loader: PageLoader = Arc::new(|_| {}); let mut state = PickerState::new( - sessions_root.clone(), + PathBuf::from("/tmp"), FrameRequester::test_dummy(), loader, String::from("openai"), @@ -1372,7 +1372,7 @@ mod tests { ); let page = block_on_future(RolloutRecorder::list_conversations( - &sessions_root, + &state.codex_home, PAGE_SIZE, None, INTERACTIVE_SESSION_SOURCES, diff --git a/docs/slash_commands.md b/docs/slash_commands.md index 35cfc52b09..d0232f2fcb 100644 --- a/docs/slash_commands.md +++ b/docs/slash_commands.md @@ -16,7 +16,7 @@ Control Codex’s behavior during an interactive session with slash commands. | `/approvals` | choose what Codex can do without approval | | `/review` | review my current changes and find issues | | `/new` | start a new chat during a conversation | -| `/resume` | resume a saved chat | +| `/resume` | resume an old chat | | `/init` | create an AGENTS.md file with instructions for Codex | | `/compact` | summarize conversation to prevent hitting the context limit | | `/undo` | ask Codex to undo a turn | From 006b5d03d66a9073620400e0ced2ff328c466328 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 2 Dec 2025 11:32:42 -0800 Subject: [PATCH 7/8] resume --- docs/slash_commands.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/slash_commands.md b/docs/slash_commands.md index d0232f2fcb..c8370fb03b 100644 --- a/docs/slash_commands.md +++ b/docs/slash_commands.md @@ -10,23 +10,23 @@ Slash commands are special commands you can type that start with `/`. Control Codex’s behavior during an interactive session with slash commands. -| Command | Purpose | -| ------------ | ----------------------------------------------------------- | -| `/model` | choose what model and reasoning effort to use | -| `/approvals` | choose what Codex can do without approval | -| `/review` | review my current changes and find issues | -| `/new` | start a new chat during a conversation | -| `/resume` | resume an old chat | -| `/init` | create an AGENTS.md file with instructions for Codex | -| `/compact` | summarize conversation to prevent hitting the context limit | -| `/undo` | ask Codex to undo a turn | -| `/diff` | show git diff (including untracked files) | -| `/mention` | mention a file | -| `/status` | show current session configuration and token usage | -| `/mcp` | list configured MCP tools | -| `/logout` | log out of Codex | -| `/quit` | exit Codex | -| `/exit` | exit Codex | -| `/feedback` | send logs to maintainers | +| Command | Purpose | +| --- | --- | +| `/model` | choose what model and reasoning effort to use | +| `/approvals` | choose what Codex can do without approval | +| `/review` | review my current changes and find issues | +| `/new` | start a new chat during a conversation | +| `/resume` | resume an old chat | +| `/init` | create an AGENTS.md file with instructions for Codex | +| `/compact` | summarize conversation to prevent hitting the context limit | +| `/undo` | ask Codex to undo a turn | +| `/diff` | show git diff (including untracked files) | +| `/mention` | mention a file | +| `/status` | show current session configuration and token usage | +| `/mcp` | list configured MCP tools | +| `/logout` | log out of Codex | +| `/quit` | exit Codex | +| `/exit` | exit Codex | +| `/feedback` | send logs to maintainers | --- From 3346af106cc85d34cb9e13c69d130b5ffc7f2b04 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 2 Dec 2025 11:35:39 -0800 Subject: [PATCH 8/8] resume --- docs/slash_commands.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/slash_commands.md b/docs/slash_commands.md index c8370fb03b..6961461d42 100644 --- a/docs/slash_commands.md +++ b/docs/slash_commands.md @@ -10,23 +10,23 @@ Slash commands are special commands you can type that start with `/`. Control Codex’s behavior during an interactive session with slash commands. -| Command | Purpose | -| --- | --- | -| `/model` | choose what model and reasoning effort to use | -| `/approvals` | choose what Codex can do without approval | -| `/review` | review my current changes and find issues | -| `/new` | start a new chat during a conversation | -| `/resume` | resume an old chat | -| `/init` | create an AGENTS.md file with instructions for Codex | -| `/compact` | summarize conversation to prevent hitting the context limit | -| `/undo` | ask Codex to undo a turn | -| `/diff` | show git diff (including untracked files) | -| `/mention` | mention a file | -| `/status` | show current session configuration and token usage | -| `/mcp` | list configured MCP tools | -| `/logout` | log out of Codex | -| `/quit` | exit Codex | -| `/exit` | exit Codex | -| `/feedback` | send logs to maintainers | +| Command | Purpose | +| ------------ | ----------------------------------------------------------- | +| `/model` | choose what model and reasoning effort to use | +| `/approvals` | choose what Codex can do without approval | +| `/review` | review my current changes and find issues | +| `/new` | start a new chat during a conversation | +| `/resume` | resume an old chat | +| `/init` | create an AGENTS.md file with instructions for Codex | +| `/compact` | summarize conversation to prevent hitting the context limit | +| `/undo` | ask Codex to undo a turn | +| `/diff` | show git diff (including untracked files) | +| `/mention` | mention a file | +| `/status` | show current session configuration and token usage | +| `/mcp` | list configured MCP tools | +| `/logout` | log out of Codex | +| `/quit` | exit Codex | +| `/exit` | exit Codex | +| `/feedback` | send logs to maintainers | ---