Skip to main content

codex_utils_cli/
resume_command.rs

1//! Shared formatting for user-facing `codex resume` command hints.
2
3use codex_protocol::ThreadId;
4use codex_shell_command::parse_command::shlex_join;
5
6pub fn resume_command(thread_name: Option<&str>, thread_id: Option<ThreadId>) -> Option<String> {
7    let resume_target = thread_name
8        .filter(|name| !name.is_empty())
9        .map(str::to_string)
10        .or_else(|| thread_id.map(|thread_id| thread_id.to_string()));
11    resume_target.map(|target| {
12        let needs_double_dash = target.starts_with('-');
13        let escaped = shlex_join(&[target]);
14        if needs_double_dash {
15            format!("codex resume -- {escaped}")
16        } else {
17            format!("codex resume {escaped}")
18        }
19    })
20}
21
22pub fn resume_hint(thread_name: Option<&str>, thread_id: Option<ThreadId>) -> Option<String> {
23    let thread_id = thread_id?;
24    match thread_name.filter(|name| !name.is_empty()) {
25        Some(thread_name) => Some(format!(
26            "codex resume, then select {thread_name} ({thread_id})"
27        )),
28        None => resume_command(/*thread_name*/ None, Some(thread_id)),
29    }
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35    use pretty_assertions::assert_eq;
36
37    #[test]
38    fn prefers_name_over_id() {
39        let thread_id = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap();
40        let command = resume_command(Some("my-thread"), Some(thread_id));
41        assert_eq!(command, Some("codex resume my-thread".to_string()));
42    }
43
44    #[test]
45    fn formats_thread_id_when_name_is_missing() {
46        let thread_id = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap();
47        let command = resume_command(/*thread_name*/ None, Some(thread_id));
48        assert_eq!(
49            command,
50            Some("codex resume 123e4567-e89b-12d3-a456-426614174000".to_string())
51        );
52    }
53
54    #[test]
55    fn returns_none_without_a_resume_target() {
56        let command = resume_command(/*thread_name*/ None, /*thread_id*/ None);
57        assert_eq!(command, None);
58    }
59
60    #[test]
61    fn quotes_thread_names_when_needed() {
62        let command = resume_command(Some("-starts-with-dash"), /*thread_id*/ None);
63        assert_eq!(
64            command,
65            Some("codex resume -- -starts-with-dash".to_string())
66        );
67
68        let command = resume_command(Some("two words"), /*thread_id*/ None);
69        assert_eq!(command, Some("codex resume 'two words'".to_string()));
70
71        let command = resume_command(Some("quote'case"), /*thread_id*/ None);
72        assert_eq!(command, Some("codex resume \"quote'case\"".to_string()));
73    }
74
75    #[test]
76    fn resume_hint_names_picker_item_with_id() {
77        let thread_id = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap();
78        let hint = resume_hint(Some("my-thread"), Some(thread_id));
79        assert_eq!(
80            hint,
81            Some(
82                "codex resume, then select my-thread (123e4567-e89b-12d3-a456-426614174000)"
83                    .to_string()
84            )
85        );
86    }
87
88    #[test]
89    fn resume_hint_uses_direct_id_command_without_name() {
90        let thread_id = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap();
91        let hint = resume_hint(/*thread_name*/ None, Some(thread_id));
92        assert_eq!(
93            hint,
94            Some("codex resume 123e4567-e89b-12d3-a456-426614174000".to_string())
95        );
96    }
97
98    #[test]
99    fn resume_hint_requires_thread_id() {
100        let hint = resume_hint(Some("my-thread"), /*thread_id*/ None);
101        assert_eq!(hint, None);
102    }
103}