Files
codex/codex-rs/utils/cli/src/resume_command.rs
Eric Traut 4ac3ea20a2 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 `<name> (<thread-id>)`.
- Uses that hint for /rename confirmations, TUI session summaries, and
CLI/TUI exit messages.
- Keeps direct `codex resume <thread-id>` guidance for unnamed threads.

## Verification
Manually verified that message after `/rename` and after `/exit` include
session ID in parens.

---------

Co-authored-by: Felipe Coury <felipe.coury@openai.com>
2026-05-18 11:32:02 -07:00

104 lines
3.5 KiB
Rust

//! Shared formatting for user-facing `codex resume` command hints.
use codex_protocol::ThreadId;
use codex_shell_command::parse_command::shlex_join;
pub fn resume_command(thread_name: Option<&str>, thread_id: Option<ThreadId>) -> Option<String> {
let resume_target = thread_name
.filter(|name| !name.is_empty())
.map(str::to_string)
.or_else(|| thread_id.map(|thread_id| thread_id.to_string()));
resume_target.map(|target| {
let needs_double_dash = target.starts_with('-');
let escaped = shlex_join(&[target]);
if needs_double_dash {
format!("codex resume -- {escaped}")
} else {
format!("codex resume {escaped}")
}
})
}
pub fn resume_hint(thread_name: Option<&str>, thread_id: Option<ThreadId>) -> Option<String> {
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::*;
use pretty_assertions::assert_eq;
#[test]
fn prefers_name_over_id() {
let thread_id = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap();
let command = resume_command(Some("my-thread"), Some(thread_id));
assert_eq!(command, Some("codex resume my-thread".to_string()));
}
#[test]
fn formats_thread_id_when_name_is_missing() {
let thread_id = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap();
let command = resume_command(/*thread_name*/ None, Some(thread_id));
assert_eq!(
command,
Some("codex resume 123e4567-e89b-12d3-a456-426614174000".to_string())
);
}
#[test]
fn returns_none_without_a_resume_target() {
let command = resume_command(/*thread_name*/ None, /*thread_id*/ None);
assert_eq!(command, None);
}
#[test]
fn quotes_thread_names_when_needed() {
let command = resume_command(Some("-starts-with-dash"), /*thread_id*/ None);
assert_eq!(
command,
Some("codex resume -- -starts-with-dash".to_string())
);
let command = resume_command(Some("two words"), /*thread_id*/ None);
assert_eq!(command, Some("codex resume 'two words'".to_string()));
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);
}
}