From 698722c9464d1fd7b9dc6181667fcab29f1871c2 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 24 May 2026 15:36:54 -0500 Subject: [PATCH] feat(cli): --continue/-c flag forwards to TUI resume path Other agent: root_tui_passthrough() builds forwarded args, rejects --continue + -p combo (directs to codewhale exec --continue). Tests: parses_top_level_continue, top_level_continue_rejects_one_shot. Session picker: formatting cleanup on test calls. --- crates/cli/src/lib.rs | 71 ++++++++++++++++++++++------ crates/tui/src/tui/session_picker.rs | 27 +++++++++-- 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 5fdbea64..c27d699f 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -102,6 +102,9 @@ struct Cli { /// YOLO mode: auto-approve all tools #[arg(long)] yolo: bool, + /// Continue the most recent interactive session for this workspace. + #[arg(short = 'c', long = "continue")] + continue_session: bool, #[arg(short = 'p', long = "prompt", value_name = "PROMPT")] prompt_flag: Option, #[arg( @@ -555,26 +558,42 @@ fn run() -> Result<()> { Some(Commands::Update) => update::run_update(), None => { let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides); - let mut forwarded = Vec::new(); - let prompt = cli.prompt_flag.iter().chain(cli.prompt.iter()).fold( - String::new(), - |mut acc, part| { - if !acc.is_empty() { - acc.push(' '); - } - acc.push_str(part); - acc - }, - ); - if !prompt.is_empty() { - forwarded.push("--prompt".to_string()); - forwarded.push(prompt); - } + let forwarded = root_tui_passthrough(&cli)?; delegate_to_tui(&cli, &resolved_runtime, forwarded) } } } +fn root_tui_passthrough(cli: &Cli) -> Result> { + let mut forwarded = Vec::new(); + if cli.continue_session { + forwarded.push("--continue".to_string()); + } + + let prompt = + cli.prompt_flag + .iter() + .chain(cli.prompt.iter()) + .fold(String::new(), |mut acc, part| { + if !acc.is_empty() { + acc.push(' '); + } + acc.push_str(part); + acc + }); + if !prompt.is_empty() { + if cli.continue_session { + bail!( + "`codewhale --continue` resumes the interactive TUI. Use `codewhale exec --continue ` to continue a session non-interactively." + ); + } + forwarded.push("--prompt".to_string()); + forwarded.push(prompt); + } + + Ok(forwarded) +} + fn resolve_runtime_for_dispatch( store: &mut ConfigStore, runtime_overrides: &CliRuntimeOverrides, @@ -2651,6 +2670,27 @@ mod tests { assert!(cli.prompt.is_empty()); } + #[test] + fn parses_top_level_continue_for_interactive_resume() { + let cli = parse_ok(&["codewhale", "--continue"]); + + assert!(cli.continue_session); + assert!(cli.prompt_flag.is_none()); + assert!(cli.prompt.is_empty()); + assert_eq!(root_tui_passthrough(&cli).unwrap(), vec!["--continue"]); + } + + #[test] + fn top_level_continue_rejects_one_shot_prompt() { + let cli = parse_ok(&["codewhale", "--continue", "-p", "follow up"]); + + let err = root_tui_passthrough(&cli).expect_err("prompted continue should be rejected"); + assert!( + err.to_string() + .contains("codewhale exec --continue ") + ); + } + #[test] fn parses_split_top_level_prompt_words_for_windows_cmd_shims() { let cli = parse_ok(&["deepseek", "hello", "world"]); @@ -2711,6 +2751,7 @@ mod tests { "--mouse-capture", "--no-mouse-capture", "--skip-onboarding", + "--continue", "--prompt", ] { assert!( diff --git a/crates/tui/src/tui/session_picker.rs b/crates/tui/src/tui/session_picker.rs index 33ed6ba4..f6a80639 100644 --- a/crates/tui/src/tui/session_picker.rs +++ b/crates/tui/src/tui/session_picker.rs @@ -1069,7 +1069,9 @@ mod tests { "A very long title that should be truncated by the list pane width", )]; let width = 24; - let lines = build_list_lines(&sessions, 0, width, 0, 5, false, "", "recent", false, false, "", None); + let lines = build_list_lines( + &sessions, 0, width, 0, 5, false, "", "recent", false, false, "", None, + ); for line in lines { let rendered_width: usize = line.spans.iter().map(|span| span.content.width()).sum(); @@ -1086,7 +1088,9 @@ mod tests { test_session(1, "first session"), test_session(2, "second session"), ]; - let lines = build_list_lines(&sessions, 1, 80, 0, 5, false, "", "recent", false, false, "", None); + let lines = build_list_lines( + &sessions, 1, 80, 0, 5, false, "", "recent", false, false, "", None, + ); let selected_line = lines .iter() @@ -1111,7 +1115,20 @@ mod tests { let mut forked = test_session(1, "forked path"); forked.parent_session_id = Some("parent-session-abcdef".to_string()); forked.forked_from_message_count = Some(3); - let lines = build_list_lines(&[forked], 0, 120, 0, 5, false, "", "recent", false, false, "", None); + let lines = build_list_lines( + &[forked], + 0, + 120, + 0, + 5, + false, + "", + "recent", + false, + false, + "", + None, + ); let rendered = lines .iter() @@ -1128,7 +1145,9 @@ mod tests { test_session(1, "first session"), test_session(2, "second session"), ]; - let lines = build_list_lines(&sessions, 0, 80, 0, 5, false, "", "recent", false, false, "", None); + let lines = build_list_lines( + &sessions, 0, 80, 0, 5, false, "", "recent", false, false, "", None, + ); let rendered = lines .iter()