From 0e03f88a30bbce4638aeabc62557dda90612d32c Mon Sep 17 00:00:00 2001 From: Benjamin Carlsson Date: Mon, 31 Aug 2026 23:27:58 +0000 Subject: [PATCH] Clarify resume guidance in exit summaries (#41938) ## What changed - Show the exact `codex resume ` command on its own indented line. - When a thread has a name, also explain that users can run `codex resume` and select the named thread from the picker. - Apply command highlighting to both resume options when color is enabled. ## Testing - Cover unnamed, empty-name, named, colored, and embedded-session exit summaries. GitOrigin-RevId: d97c04db8a34376c6d9ea839b515b04a384b85d1 --- codex-rs/cli/src/main.rs | 65 +++++++++++++------ codex-rs/tui/src/app.rs | 9 +-- codex-rs/tui/src/app/exit_summary.rs | 24 +++++-- .../src/app/tests/background_exit_tests.rs | 23 ++++--- codex-rs/tui/src/lib.rs | 1 + 5 files changed, 82 insertions(+), 40 deletions(-) diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 0bfea04498..71d0323735 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -3735,7 +3735,10 @@ mod tests { AppExitInfo { token_usage, thread_id, - resume_hint: codex_utils_cli::resume_hint(thread_name, thread_id), + resume_hint: thread_id.map(|thread_id| codex_tui::ResumableThread { + thread_id, + thread_name: thread_name.map(str::to_string), + }), disconnect_info: None, update_action: None, exit_reason: ExitReason::UserRequested, @@ -3844,27 +3847,26 @@ mod tests { lines, vec![ "Token usage: total=2 input=0 output=2".to_string(), - "To continue this session, run codex resume 123e4567-e89b-12d3-a456-426614174000" - .to_string(), + "To continue this session, run:".to_string(), + " codex resume 123e4567-e89b-12d3-a456-426614174000".to_string(), ] ); } #[test] fn format_exit_messages_includes_resume_hint_without_color() { - let exit_info = sample_exit_info( - Some("123e4567-e89b-12d3-a456-426614174000"), - /*thread_name*/ None, - ); - let lines = exit_info.format_exit_messages(/*color_enabled*/ false); - assert_eq!( - lines, - vec![ - "Token usage: total=2 input=0 output=2".to_string(), - "To continue this session, run codex resume 123e4567-e89b-12d3-a456-426614174000" - .to_string(), - ] - ); + insta::allow_duplicates! { + for thread_name in [None, Some("")] { + let exit_info = + sample_exit_info(Some("123e4567-e89b-12d3-a456-426614174000"), thread_name); + let lines = exit_info.format_exit_messages(/*color_enabled*/ false); + insta::assert_snapshot!(lines.join("\n"), @" + Token usage: total=2 input=0 output=2 + To continue this session, run: + codex resume 123e4567-e89b-12d3-a456-426614174000 + "); + } + } } #[test] @@ -3874,8 +3876,14 @@ mod tests { /*thread_name*/ None, ); let lines = exit_info.format_exit_messages(/*color_enabled*/ true); - assert_eq!(lines.len(), 2); - assert!(lines[1].contains("\u{1b}[36m")); + assert_eq!( + lines, + vec![ + "Token usage: total=2 input=0 output=2", + "To continue this session, run:", + " \u{1b}[36mcodex resume 123e4567-e89b-12d3-a456-426614174000\u{1b}[39m", + ] + ); } #[test] @@ -3885,11 +3893,28 @@ mod tests { Some("my-thread"), ); let lines = exit_info.format_exit_messages(/*color_enabled*/ false); + insta::assert_snapshot!(lines.join("\n"), @" + Token usage: total=2 input=0 output=2 + To continue this session, run: + codex resume 123e4567-e89b-12d3-a456-426614174000 + Or run codex resume and select my-thread. + "); + } + + #[test] + fn format_exit_messages_colors_commands_and_thread_name() { + let exit_info = sample_exit_info( + Some("123e4567-e89b-12d3-a456-426614174000"), + Some("my-thread"), + ); + let lines = exit_info.format_exit_messages(/*color_enabled*/ true); assert_eq!( lines, vec![ - "Token usage: total=2 input=0 output=2".to_string(), - "To continue this session, run codex resume, then select my-thread (123e4567-e89b-12d3-a456-426614174000)".to_string(), + "Token usage: total=2 input=0 output=2", + "To continue this session, run:", + " \u{1b}[36mcodex resume 123e4567-e89b-12d3-a456-426614174000\u{1b}[39m", + "Or run \u{1b}[36mcodex resume\u{1b}[39m and select \u{1b}[36mmy-thread\u{1b}[39m.", ] ); } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index e2e909f71e..0162bbc101 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -423,7 +423,7 @@ const COMMIT_ANIMATION_TICK: Duration = tui::TARGET_FRAME_INTERVAL; pub struct AppExitInfo { pub token_usage: TokenUsage, pub thread_id: Option, - pub resume_hint: Option, + pub resume_hint: Option, pub disconnect_info: Option, pub update_action: Option, pub exit_reason: ExitReason, @@ -443,6 +443,7 @@ impl AppExitInfo { } pub use exit_summary::DisconnectInfo; +pub use exit_summary::ResumableThread; #[derive(Debug)] pub(crate) enum AppRunControl { @@ -479,12 +480,6 @@ fn session_summary( }) } -#[derive(Debug, Clone, PartialEq, Eq)] -struct ResumableThread { - thread_id: ThreadId, - thread_name: Option, -} - fn resumable_thread( thread_id: Option, thread_name: Option, diff --git a/codex-rs/tui/src/app/exit_summary.rs b/codex-rs/tui/src/app/exit_summary.rs index 6b23db555a..39b023805b 100644 --- a/codex-rs/tui/src/app/exit_summary.rs +++ b/codex-rs/tui/src/app/exit_summary.rs @@ -1,10 +1,18 @@ //! Exit output distinguishes a disconnected client from a stopped embedded server. +//! Persisted sessions offer a direct UUID command and an optional named picker hint. use super::*; use crate::RemoteAppServerEndpoint; use crate::exec_command::escape_command; use crate::status::remote_connection::sanitized_websocket_url; +/// A persisted thread that can be resumed after the TUI exits. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResumableThread { + pub thread_id: ThreadId, + pub thread_name: Option, +} + /// Reconnection and stop guidance for a task owned by a persistent app server. #[derive(Debug, Clone)] pub struct DisconnectInfo { @@ -57,7 +65,7 @@ impl App { AppExitInfo { token_usage: self.token_usage(), thread_id, - resume_hint: resume_hint_for_resumable_thread( + resume_hint: resumable_thread( thread_id, self.chat_widget.thread_name(), self.chat_widget.rollout_path().as_deref(), @@ -125,11 +133,19 @@ impl AppExitInfo { } if let ExitReason::Archived(thread_id) = self.exit_reason { lines.push(format!("Session archived: {thread_id}")); - } else if let Some(resume_command) = self.resume_hint { + } else if let Some(thread) = self.resume_hint { + lines.push("To continue this session, run:".to_string()); lines.push(format!( - "To continue this session, run {}", - color_command(resume_command), + " {}", + color_command(format!("codex resume {}", thread.thread_id)), )); + if let Some(thread_name) = thread.thread_name.filter(|name| !name.is_empty()) { + lines.push(format!( + "Or run {} and select {}.", + color_command("codex resume".to_string()), + color_command(thread_name), + )); + } } else if let Some(thread_id) = self.thread_id { lines.push(format!("Session ID: {thread_id}")); } diff --git a/codex-rs/tui/src/app/tests/background_exit_tests.rs b/codex-rs/tui/src/app/tests/background_exit_tests.rs index d693d27f90..7738e20657 100644 --- a/codex-rs/tui/src/app/tests/background_exit_tests.rs +++ b/codex-rs/tui/src/app/tests/background_exit_tests.rs @@ -52,7 +52,7 @@ async fn remote_disconnect_exit_summary_does_not_require_a_local_rollout_or_prin #[tokio::test] async fn embedded_exit_keeps_the_session_summary() { let (mut app, _, _) = make_test_app_with_channels().await; - prepare_local_daemon_thread(&mut app).unwrap(); + let thread_id = prepare_local_daemon_thread(&mut app).unwrap(); app.app_server_target = AppServerTarget::Embedded; let mut exit_info = app.exit_info(ExitReason::UserRequested); exit_info.token_usage = TokenUsage { @@ -60,14 +60,19 @@ async fn embedded_exit_keeps_the_session_summary() { total_tokens: 2, ..Default::default() }; - exit_info.resume_hint = Some("codex resume THREAD_ID".to_string()); - assert_eq!( - exit_info.format_exit_messages(/*color_enabled*/ false), - vec![ - "Token usage: total=2 input=0 output=2", - "To continue this session, run codex resume THREAD_ID", - ] - ); + exit_info.resume_hint = Some(ResumableThread { + thread_id, + thread_name: None, + }); + let output = exit_info + .format_exit_messages(/*color_enabled*/ false) + .join("\n") + .replace(&thread_id.to_string(), "THREAD_ID"); + assert_snapshot!(output, @" + Token usage: total=2 input=0 output=2 + To continue this session, run: + codex resume THREAD_ID + "); } fn prepare_local_daemon_thread(app: &mut App) -> Result { diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 5cbf403654..36852ab013 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -23,6 +23,7 @@ use app::App; pub use app::AppExitInfo; pub use app::DisconnectInfo; pub use app::ExitReason; +pub use app::ResumableThread; use app_server_session::AppServerSession; use app_server_session::ThreadParamsMode; use codex_app_server_client::AppServerClient;