From 4ac3ea20a28d8b5239e57ca01f0f4a0acf59baaa Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 18 May 2026 11:32:02 -0700 Subject: [PATCH] Clarify resume hints for renamed threads (#23234) Addresses #23181 ## Why Renamed threads can share names, so hints that suggest resuming directly by name are ambiguous. Issue #23181 asks for the picker hint to include the thread name and thread ID in parens so users can disambiguate safely. ## What - Adds a shared resume hint formatter for named threads: run `codex resume`, then select ` ()`. - Uses that hint for /rename confirmations, TUI session summaries, and CLI/TUI exit messages. - Keeps direct `codex resume ` guidance for unnamed threads. ## Verification Manually verified that message after `/rename` and after `/exit` include session ID in parens. --------- Co-authored-by: Felipe Coury --- codex-rs/cli/src/main.rs | 10 ++--- codex-rs/tui/src/app.rs | 13 ++++--- codex-rs/tui/src/app/event_dispatch.rs | 2 +- codex-rs/tui/src/app/session_lifecycle.rs | 4 +- codex-rs/tui/src/app/tests/session_summary.rs | 11 ++++-- codex-rs/tui/src/chatwidget.rs | 14 +++---- ...tests__thread_name_update_resume_hint.snap | 5 +++ .../tui/src/chatwidget/tests/app_server.rs | 6 +-- codex-rs/tui/src/main.rs | 5 ++- codex-rs/tui/src/resume_picker.rs | 16 ++++---- codex-rs/utils/cli/src/lib.rs | 1 + codex-rs/utils/cli/src/resume_command.rs | 39 +++++++++++++++++++ 12 files changed, 88 insertions(+), 38 deletions(-) create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__thread_name_update_resume_hint.snap diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 8274510b9..4b0d2330a 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -38,7 +38,7 @@ use codex_tui::UpdateAction; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_cli::CliConfigOverrides; use codex_utils_cli::ProfileV2Name; -use codex_utils_cli::resume_command; +use codex_utils_cli::resume_hint; use owo_colors::OwoColorize; use std::io::IsTerminal; use std::path::PathBuf; @@ -629,6 +629,7 @@ fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec Vec, ) -> Option { let usage_line = (!token_usage.is_zero()).then(|| token_usage.to_string()); - let thread_id = - resumable_thread(thread_id, thread_name, rollout_path).map(|thread| thread.thread_id); - let resume_command = codex_utils_cli::resume_command(/*thread_name*/ None, thread_id); + let resumable_thread = resumable_thread(thread_id, thread_name, rollout_path); + let resume_hint = resumable_thread.as_ref().and_then(|thread| { + codex_utils_cli::resume_hint(thread.thread_name.as_deref(), Some(thread.thread_id)) + }); - if usage_line.is_none() && resume_command.is_none() { + if usage_line.is_none() && resume_hint.is_none() { return None; } Some(SessionSummary { usage_line, - resume_command, + resume_hint, }) } @@ -459,7 +460,7 @@ fn errors_for_cwd(cwd: &Path, response: &SkillsListResponse) -> Vec, - resume_command: Option, + resume_hint: Option, } #[derive(Debug, Default)] diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index d1ce849c0..ddb11c452 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -152,7 +152,7 @@ impl App { if let Some(usage_line) = summary.usage_line { lines.push(usage_line.into()); } - if let Some(command) = summary.resume_command { + if let Some(command) = summary.resume_hint { let spans = vec![ "To continue this session, run ".into(), command.cyan(), diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index 75af2f1da..1a57b596b 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -473,7 +473,7 @@ impl App { if let Some(usage_line) = summary.usage_line { lines.push(usage_line.into()); } - if let Some(command) = summary.resume_command { + if let Some(command) = summary.resume_hint { let spans = vec!["To continue this session, run ".into(), command.cyan()]; lines.push(spans.into()); } @@ -701,7 +701,7 @@ impl App { if let Some(usage_line) = summary.usage_line { lines.push(usage_line.into()); } - if let Some(command) = summary.resume_command { + if let Some(command) = summary.resume_hint { let spans = vec!["To continue this session, run ".into(), command.cyan()]; lines.push(spans.into()); diff --git a/codex-rs/tui/src/app/tests/session_summary.rs b/codex-rs/tui/src/app/tests/session_summary.rs index 2a96f9ad3..63ff92a09 100644 --- a/codex-rs/tui/src/app/tests/session_summary.rs +++ b/codex-rs/tui/src/app/tests/session_summary.rs @@ -57,13 +57,13 @@ async fn session_summary_includes_resume_hint_for_persisted_rollout() { Some("Token usage: total=12 input=10 output=2".to_string()) ); assert_eq!( - summary.resume_command, + summary.resume_hint, Some("codex resume 123e4567-e89b-12d3-a456-426614174000".to_string()) ); } #[tokio::test] -async fn session_summary_uses_id_even_when_thread_has_name() { +async fn session_summary_names_picker_item_when_thread_has_name() { let usage = TokenUsage { input_tokens: 10, output_tokens: 2, @@ -83,7 +83,10 @@ async fn session_summary_uses_id_even_when_thread_has_name() { ) .expect("summary"); assert_eq!( - summary.resume_command, - Some("codex resume 123e4567-e89b-12d3-a456-426614174000".to_string()) + summary.resume_hint, + Some( + "codex resume, then select my-session (123e4567-e89b-12d3-a456-426614174000)" + .to_string() + ) ); } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 8c84748dd..d42016291 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -166,7 +166,7 @@ use codex_terminal_detection::TerminalInfo; use codex_terminal_detection::TerminalName; use codex_terminal_detection::terminal_info; use codex_utils_absolute_path::AbsolutePathBuf; -use codex_utils_cli::resume_command; +use codex_utils_cli::resume_hint; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; @@ -1440,16 +1440,14 @@ impl ChatWidget { } fn rename_confirmation_cell(name: &str, thread_id: Option) -> PlainHistoryCell { - let resume_cmd = - resume_command(Some(name), thread_id).unwrap_or_else(|| format!("codex resume {name}")); - let name = name.to_string(); - let line = vec![ + let mut line = vec![ "• ".into(), "Thread renamed to ".into(), - name.cyan(), - ", to resume this thread run ".into(), - resume_cmd.cyan(), + name.to_string().cyan(), ]; + if let Some(hint) = resume_hint(Some(name), thread_id) { + line.extend([". To resume this thread run ".into(), hint.cyan()]); + } PlainHistoryCell::new(vec![line.into()]) } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__thread_name_update_resume_hint.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__thread_name_update_resume_hint.snap new file mode 100644 index 000000000..b3cdf89da --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__thread_name_update_resume_hint.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/chatwidget/tests/app_server.rs +expression: rendered +--- +• Thread renamed to review-fix. To resume this thread run codex resume, then select review-fix (123e4567-e89b-12d3-a456-426614174000) diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index 688e30bd1..91c5b1d1f 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -861,7 +861,8 @@ async fn live_app_server_invalid_thread_name_update_is_ignored() { #[tokio::test] async fn live_app_server_thread_name_update_shows_resume_hint() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - let thread_id = ThreadId::new(); + let thread_id = + ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").expect("thread id"); chat.thread_id = Some(thread_id); chat.handle_server_notification( @@ -878,8 +879,7 @@ async fn live_app_server_thread_name_update_shows_resume_hint() { let cells = drain_insert_history(&mut rx); assert_eq!(cells.len(), 1); let rendered = lines_to_single_string(&cells[0]); - assert!(rendered.contains("Thread renamed to review-fix")); - assert!(rendered.contains("codex resume review-fix")); + assert_chatwidget_snapshot!("thread_name_update_resume_hint", rendered); } #[tokio::test] diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs index e41d5f5a7..9c69c7c81 100644 --- a/codex-rs/tui/src/main.rs +++ b/codex-rs/tui/src/main.rs @@ -7,13 +7,14 @@ use codex_tui::Cli; use codex_tui::ExitReason; use codex_tui::run_main; use codex_utils_cli::CliConfigOverrides; -use codex_utils_cli::resume_command; +use codex_utils_cli::resume_hint; use supports_color::Stream; fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec { let AppExitInfo { token_usage, thread_id, + thread_name, .. } = exit_info; @@ -22,7 +23,7 @@ fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec Vec> { let reference = state.relative_time_reference.unwrap_or_else(Utc::now); - let session = row - .thread_name - .as_deref() - .map(str::to_string) - .or_else(|| row.thread_id.map(|thread_id| thread_id.to_string())) - .unwrap_or_else(|| "-".to_string()); + let session = match (row.thread_name.as_deref(), row.thread_id) { + (Some(thread_name), Some(thread_id)) => format!("{thread_name} ({thread_id})"), + (Some(thread_name), None) => thread_name.to_string(), + (None, Some(thread_id)) => thread_id.to_string(), + (None, None) => "-".to_string(), + }; let directory = row .cwd .as_ref() @@ -3387,7 +3387,9 @@ mod tests { let expected_directory = format_directory_display(row.cwd.as_deref().expect("cwd"), /*max_width*/ None); - assert!(rendered.contains("Session: feat(tui): add raw scrollback mode")); + assert!(rendered.contains( + "Session: feat(tui): add raw scrollback mode (019dabc1-0ef5-7431-b81c-03037f51f62c)" + )); assert!(rendered.contains("Created: 17 minutes ago · 2026-05-02 14:31:08")); assert!(rendered.contains("Updated: now · 2026-05-02 14:48:19")); assert!(rendered.contains(&format!("Directory: {expected_directory}"))); diff --git a/codex-rs/utils/cli/src/lib.rs b/codex-rs/utils/cli/src/lib.rs index 32ef52e09..633638e72 100644 --- a/codex-rs/utils/cli/src/lib.rs +++ b/codex-rs/utils/cli/src/lib.rs @@ -10,5 +10,6 @@ pub use codex_protocol::config_types::ProfileV2Name; pub use config_override::CliConfigOverrides; pub use format_env_display::format_env_display; pub use resume_command::resume_command; +pub use resume_command::resume_hint; pub use sandbox_mode_cli_arg::SandboxModeCliArg; pub use shared_options::SharedCliOptions; diff --git a/codex-rs/utils/cli/src/resume_command.rs b/codex-rs/utils/cli/src/resume_command.rs index 52b88e0e3..d3c683905 100644 --- a/codex-rs/utils/cli/src/resume_command.rs +++ b/codex-rs/utils/cli/src/resume_command.rs @@ -19,6 +19,16 @@ pub fn resume_command(thread_name: Option<&str>, thread_id: Option) -> }) } +pub fn resume_hint(thread_name: Option<&str>, thread_id: Option) -> Option { + let thread_id = thread_id?; + match thread_name.filter(|name| !name.is_empty()) { + Some(thread_name) => Some(format!( + "codex resume, then select {thread_name} ({thread_id})" + )), + None => resume_command(/*thread_name*/ None, Some(thread_id)), + } +} + #[cfg(test)] mod tests { use super::*; @@ -61,4 +71,33 @@ mod tests { let command = resume_command(Some("quote'case"), /*thread_id*/ None); assert_eq!(command, Some("codex resume \"quote'case\"".to_string())); } + + #[test] + fn resume_hint_names_picker_item_with_id() { + let thread_id = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap(); + let hint = resume_hint(Some("my-thread"), Some(thread_id)); + assert_eq!( + hint, + Some( + "codex resume, then select my-thread (123e4567-e89b-12d3-a456-426614174000)" + .to_string() + ) + ); + } + + #[test] + fn resume_hint_uses_direct_id_command_without_name() { + let thread_id = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap(); + let hint = resume_hint(/*thread_name*/ None, Some(thread_id)); + assert_eq!( + hint, + Some("codex resume 123e4567-e89b-12d3-a456-426614174000".to_string()) + ); + } + + #[test] + fn resume_hint_requires_thread_id() { + let hint = resume_hint(Some("my-thread"), /*thread_id*/ None); + assert_eq!(hint, None); + } }