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