Skip to main content

codex_wrapper/command/
resume.rs

1/// Resume a previous interactive session.
2///
3/// Wraps the top-level `codex resume` command (distinct from `codex exec resume`).
4use crate::Codex;
5use crate::command::CodexCommand;
6use crate::command::exec::effective_sandbox;
7use crate::error::Result;
8use crate::exec::{self, CommandOutput};
9use crate::types::{ApprovalPolicy, SandboxMode};
10
11/// Resume a previous interactive Codex session.
12#[derive(Debug, Clone)]
13pub struct ResumeCommand {
14    approve_for_me: bool,
15    session_id: Option<String>,
16    prompt: Option<String>,
17    last: bool,
18    all: bool,
19    config_overrides: Vec<String>,
20    enabled_features: Vec<String>,
21    disabled_features: Vec<String>,
22    images: Vec<String>,
23    model: Option<String>,
24    oss: bool,
25    local_provider: Option<String>,
26    profile: Option<String>,
27    sandbox: Option<SandboxMode>,
28    approval_policy: Option<ApprovalPolicy>,
29    full_auto: bool,
30    dangerously_bypass_approvals_and_sandbox: bool,
31    dangerously_bypass_hook_trust: bool,
32    strict_config: bool,
33    no_alt_screen: bool,
34    include_non_interactive: bool,
35    remote: Option<String>,
36    remote_auth_token_env: Option<String>,
37    cd: Option<String>,
38    search: bool,
39    add_dirs: Vec<String>,
40}
41
42impl ResumeCommand {
43    #[must_use]
44    pub fn new() -> Self {
45        Self {
46            approve_for_me: false,
47            session_id: None,
48            prompt: None,
49            last: false,
50            all: false,
51            config_overrides: Vec::new(),
52            enabled_features: Vec::new(),
53            disabled_features: Vec::new(),
54            images: Vec::new(),
55            model: None,
56            oss: false,
57            local_provider: None,
58            profile: None,
59            sandbox: None,
60            approval_policy: None,
61            full_auto: false,
62            dangerously_bypass_approvals_and_sandbox: false,
63            dangerously_bypass_hook_trust: false,
64            strict_config: false,
65            no_alt_screen: false,
66            include_non_interactive: false,
67            remote: None,
68            remote_auth_token_env: None,
69            cd: None,
70            search: false,
71            add_dirs: Vec::new(),
72        }
73    }
74
75    /// Session ID (UUID) or thread name to resume.
76    #[must_use]
77    pub fn session_id(mut self, id: impl Into<String>) -> Self {
78        self.session_id = Some(id.into());
79        self
80    }
81
82    /// Optional prompt to start the resumed session with.
83    #[must_use]
84    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
85        self.prompt = Some(prompt.into());
86        self
87    }
88
89    /// Continue the most recent session without showing the picker.
90    #[must_use]
91    pub fn last(mut self) -> Self {
92        self.last = true;
93        self
94    }
95
96    /// Show all sessions (disables cwd filtering).
97    #[must_use]
98    pub fn all(mut self) -> Self {
99        self.all = true;
100        self
101    }
102
103    #[must_use]
104    pub fn config(mut self, key_value: impl Into<String>) -> Self {
105        self.config_overrides.push(key_value.into());
106        self
107    }
108
109    #[must_use]
110    pub fn enable(mut self, feature: impl Into<String>) -> Self {
111        self.enabled_features.push(feature.into());
112        self
113    }
114
115    #[must_use]
116    pub fn disable(mut self, feature: impl Into<String>) -> Self {
117        self.disabled_features.push(feature.into());
118        self
119    }
120
121    #[must_use]
122    pub fn image(mut self, path: impl Into<String>) -> Self {
123        self.images.push(path.into());
124        self
125    }
126
127    #[must_use]
128    pub fn model(mut self, model: impl Into<String>) -> Self {
129        self.model = Some(model.into());
130        self
131    }
132
133    #[must_use]
134    pub fn oss(mut self) -> Self {
135        self.oss = true;
136        self
137    }
138
139    #[must_use]
140    pub fn local_provider(mut self, provider: impl Into<String>) -> Self {
141        self.local_provider = Some(provider.into());
142        self
143    }
144
145    #[must_use]
146    pub fn profile(mut self, profile: impl Into<String>) -> Self {
147        self.profile = Some(profile.into());
148        self
149    }
150
151    #[must_use]
152    pub fn sandbox(mut self, sandbox: SandboxMode) -> Self {
153        self.sandbox = Some(sandbox);
154        self
155    }
156
157    #[must_use]
158    pub fn approval_policy(mut self, policy: ApprovalPolicy) -> Self {
159        self.approval_policy = Some(policy);
160        self
161    }
162
163    /// Run in full-auto mode, emitted as `--sandbox workspace-write`.
164    ///
165    /// `codex resume` rejects `--full-auto` outright in `codex-cli` 0.145.0
166    /// ("unexpected argument"), so this emits the replacement the CLI names
167    /// for the exec family. An explicit [`sandbox`](Self::sandbox) call is
168    /// more specific and wins over it.
169    #[must_use]
170    pub fn full_auto(mut self) -> Self {
171        self.full_auto = true;
172        self
173    }
174
175    /// Route approval requests through automatic review, using the
176    /// workspace-write sandbox (`--approve-for-me`).
177    ///
178    /// Added in `codex-cli` 0.147.0. Older releases reject it as an unexpected
179    /// argument, so this is the one builder method with a floor above the
180    /// wrapper's tested minimum. `codex exec review` and `codex exec resume`
181    /// do not accept it.
182    #[must_use]
183    pub fn approve_for_me(mut self) -> Self {
184        self.approve_for_me = true;
185        self
186    }
187
188    #[must_use]
189    pub(crate) fn set_bypass_approvals_and_sandbox(mut self) -> Self {
190        self.dangerously_bypass_approvals_and_sandbox = true;
191        self
192    }
193
194    /// Bypass the hook trust prompt (`--dangerously-bypass-hook-trust`).
195    #[must_use]
196    pub(crate) fn set_bypass_hook_trust(mut self) -> Self {
197        self.dangerously_bypass_hook_trust = true;
198        self
199    }
200
201    /// Error on unrecognized config keys (`--strict-config`).
202    #[must_use]
203    pub fn strict_config(mut self) -> Self {
204        self.strict_config = true;
205        self
206    }
207
208    /// Do not use the terminal alternate screen (`--no-alt-screen`).
209    #[must_use]
210    pub fn no_alt_screen(mut self) -> Self {
211        self.no_alt_screen = true;
212        self
213    }
214
215    /// Include non-interactive sessions in the picker
216    /// (`--include-non-interactive`).
217    #[must_use]
218    pub fn include_non_interactive(mut self) -> Self {
219        self.include_non_interactive = true;
220        self
221    }
222
223    /// Connect the TUI to a remote app server endpoint (`--remote <ADDR>`).
224    #[must_use]
225    pub fn remote(mut self, addr: impl Into<String>) -> Self {
226        self.remote = Some(addr.into());
227        self
228    }
229
230    /// Env var holding the bearer token for the remote app server
231    /// (`--remote-auth-token-env <ENV_VAR>`).
232    #[must_use]
233    pub fn remote_auth_token_env(mut self, env_var: impl Into<String>) -> Self {
234        self.remote_auth_token_env = Some(env_var.into());
235        self
236    }
237
238    #[must_use]
239    pub fn cd(mut self, dir: impl Into<String>) -> Self {
240        self.cd = Some(dir.into());
241        self
242    }
243
244    /// Enable live web search.
245    #[must_use]
246    pub fn search(mut self) -> Self {
247        self.search = true;
248        self
249    }
250
251    #[must_use]
252    pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
253        self.add_dirs.push(dir.into());
254        self
255    }
256}
257
258impl Default for ResumeCommand {
259    fn default() -> Self {
260        Self::new()
261    }
262}
263
264impl CodexCommand for ResumeCommand {
265    type Output = CommandOutput;
266
267    fn args(&self) -> Vec<String> {
268        let mut args = vec!["resume".into()];
269
270        for v in &self.config_overrides {
271            args.push("-c".into());
272            args.push(v.clone());
273        }
274        for v in &self.enabled_features {
275            args.push("--enable".into());
276            args.push(v.clone());
277        }
278        for v in &self.disabled_features {
279            args.push("--disable".into());
280            args.push(v.clone());
281        }
282        if self.last {
283            args.push("--last".into());
284        }
285        if self.all {
286            args.push("--all".into());
287        }
288        for v in &self.images {
289            args.push("--image".into());
290            args.push(v.clone());
291        }
292        if let Some(model) = &self.model {
293            args.push("--model".into());
294            args.push(model.clone());
295        }
296        if self.oss {
297            args.push("--oss".into());
298        }
299        if let Some(provider) = &self.local_provider {
300            args.push("--local-provider".into());
301            args.push(provider.clone());
302        }
303        if let Some(profile) = &self.profile {
304            args.push("--profile".into());
305            args.push(profile.clone());
306        }
307        if let Some(sandbox) = effective_sandbox(self.sandbox, self.full_auto) {
308            args.push("--sandbox".into());
309            args.push(sandbox.as_arg().into());
310        }
311        if let Some(policy) = self.approval_policy {
312            args.push("--ask-for-approval".into());
313            args.push(policy.as_arg().into());
314        }
315        if self.approve_for_me {
316            args.push("--approve-for-me".into());
317        }
318        if self.dangerously_bypass_approvals_and_sandbox {
319            args.push("--dangerously-bypass-approvals-and-sandbox".into());
320        }
321        if self.dangerously_bypass_hook_trust {
322            args.push("--dangerously-bypass-hook-trust".into());
323        }
324        if self.strict_config {
325            args.push("--strict-config".into());
326        }
327        if self.no_alt_screen {
328            args.push("--no-alt-screen".into());
329        }
330        if self.include_non_interactive {
331            args.push("--include-non-interactive".into());
332        }
333        if let Some(remote) = &self.remote {
334            args.push("--remote".into());
335            args.push(remote.clone());
336        }
337        if let Some(env_var) = &self.remote_auth_token_env {
338            args.push("--remote-auth-token-env".into());
339            args.push(env_var.clone());
340        }
341        if let Some(cd) = &self.cd {
342            args.push("--cd".into());
343            args.push(cd.clone());
344        }
345        if self.search {
346            args.push("--search".into());
347        }
348        for v in &self.add_dirs {
349            args.push("--add-dir".into());
350            args.push(v.clone());
351        }
352        if let Some(id) = &self.session_id {
353            args.push(id.clone());
354        }
355        if let Some(prompt) = &self.prompt {
356            args.push(prompt.clone());
357        }
358        args
359    }
360
361    async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
362        exec::run_codex(codex, self.args()).await
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    #[test]
371    fn resume_last_args() {
372        let args = ResumeCommand::new()
373            .last()
374            .model("gpt-5")
375            .prompt("continue")
376            .args();
377        assert_eq!(
378            args,
379            vec!["resume", "--last", "--model", "gpt-5", "continue"]
380        );
381    }
382
383    #[test]
384    fn resume_session_id_args() {
385        let args = ResumeCommand::new()
386            .session_id("abc-123")
387            .sandbox(SandboxMode::WorkspaceWrite)
388            .search()
389            .args();
390        assert_eq!(
391            args,
392            vec![
393                "resume",
394                "--sandbox",
395                "workspace-write",
396                "--search",
397                "abc-123"
398            ]
399        );
400    }
401
402    #[test]
403    fn resume_new_flags_args() {
404        let args = ResumeCommand::new()
405            .last()
406            .strict_config()
407            .include_non_interactive()
408            .no_alt_screen()
409            .remote("ws://host:9000")
410            .args();
411        assert_eq!(
412            args,
413            vec![
414                "resume",
415                "--last",
416                "--strict-config",
417                "--no-alt-screen",
418                "--include-non-interactive",
419                "--remote",
420                "ws://host:9000",
421            ]
422        );
423    }
424
425    /// `codex resume` rejects `--full-auto` outright. See #55.
426    #[test]
427    fn resume_full_auto_emits_sandbox_workspace_write() {
428        let args = ResumeCommand::new().last().full_auto().args();
429        assert_eq!(
430            args,
431            vec!["resume", "--last", "--sandbox", "workspace-write"]
432        );
433        assert!(!args.iter().any(|a| a == "--full-auto"));
434    }
435
436    #[test]
437    fn resume_explicit_sandbox_wins_over_full_auto() {
438        let args = ResumeCommand::new()
439            .last()
440            .full_auto()
441            .sandbox(SandboxMode::DangerFullAccess)
442            .args();
443        assert_eq!(
444            args,
445            vec!["resume", "--last", "--sandbox", "danger-full-access"]
446        );
447    }
448}