diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 31f186ec..1ee04c95 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -92,15 +92,14 @@ struct Cli { no_mouse_capture: bool, #[arg(long = "skip-onboarding")] skip_onboarding: bool, - #[arg( - short = 'p', - long = "prompt", - value_name = "PROMPT", - conflicts_with = "prompt" - )] + #[arg(short = 'p', long = "prompt", value_name = "PROMPT")] prompt_flag: Option, - #[arg(value_name = "PROMPT")] - prompt: Option, + #[arg( + value_name = "PROMPT", + trailing_var_arg = true, + allow_hyphen_values = true + )] + prompt: Vec, #[command(subcommand)] command: Option, } @@ -513,7 +512,17 @@ fn run() -> Result<()> { None => { let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides); let mut forwarded = Vec::new(); - if let Some(prompt) = cli.prompt_flag.clone().or_else(|| cli.prompt.clone()) { + 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); } @@ -2533,7 +2542,31 @@ mod tests { let cli = parse_ok(&["deepseek", "-p", "Reply with exactly OK."]); assert_eq!(cli.prompt_flag.as_deref(), Some("Reply with exactly OK.")); - assert_eq!(cli.prompt, None); + assert!(cli.prompt.is_empty()); + } + + #[test] + fn parses_split_top_level_prompt_words_for_windows_cmd_shims() { + let cli = parse_ok(&["deepseek", "hello", "world"]); + + assert_eq!(cli.prompt, vec!["hello", "world"]); + assert!(cli.command.is_none()); + } + + #[test] + fn prompt_flag_keeps_split_tail_words_for_windows_cmd_shims() { + let cli = parse_ok(&["deepseek", "-p", "hello", "world"]); + + assert_eq!(cli.prompt_flag.as_deref(), Some("hello")); + assert_eq!(cli.prompt, vec!["world"]); + } + + #[test] + fn known_subcommands_still_parse_before_prompt_tail() { + let cli = parse_ok(&["deepseek", "doctor"]); + + assert!(cli.prompt.is_empty()); + assert!(matches!(cli.command, Some(Commands::Doctor(_)))); } #[test] diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 2ab4f2de..1557a25a 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -110,8 +110,8 @@ struct Cli { feature_toggles: FeatureToggles, /// Send a one-shot prompt (non-interactive) - #[arg(short, long)] - prompt: Option, + #[arg(short, long, value_name = "PROMPT", num_args = 1..)] + prompt: Vec, /// YOLO mode: enable agent tools + shell execution #[arg(long)] @@ -265,7 +265,13 @@ enum Commands { #[derive(Args, Debug, Clone)] struct ExecArgs { /// Prompt to send to the model - prompt: String, + #[arg( + value_name = "PROMPT", + required = true, + trailing_var_arg = true, + allow_hyphen_values = true + )] + prompt: Vec, /// Override model for this run #[arg(long)] model: Option, @@ -277,6 +283,10 @@ struct ExecArgs { json: bool, } +fn join_prompt_parts(parts: &[String]) -> String { + parts.join(" ") +} + #[derive(Args, Debug, Clone, Default)] struct SetupArgs { /// Initialize MCP configuration at the configured path @@ -654,6 +664,7 @@ async fn main() -> Result<()> { .model .or_else(|| config.default_text_model.clone()) .unwrap_or_else(|| config.default_model()); + let prompt = join_prompt_parts(&args.prompt); if args.auto || cli.yolo { let workspace = cli.workspace.clone().unwrap_or_else(|| { std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) @@ -666,7 +677,7 @@ async fn main() -> Result<()> { run_exec_agent( &config, &model, - &args.prompt, + &prompt, workspace, max_subagents, true, @@ -675,9 +686,9 @@ async fn main() -> Result<()> { ) .await } else if args.json { - run_one_shot_json(&config, &model, &args.prompt).await + run_one_shot_json(&config, &model, &prompt).await } else { - run_one_shot(&config, &model, &args.prompt).await + run_one_shot(&config, &model, &prompt).await } } Commands::Review(args) => { @@ -765,7 +776,8 @@ async fn main() -> Result<()> { // One-shot prompt mode let config = load_config_from_cli(&cli)?; - if let Some(prompt) = cli.prompt { + if !cli.prompt.is_empty() { + let prompt = join_prompt_parts(&cli.prompt); let model = config.default_model(); return run_one_shot(&config, &model, &prompt).await; } @@ -4567,6 +4579,34 @@ mod terminal_mode_tests { Cli::try_parse_from(args).expect("CLI args should parse") } + #[test] + fn prompt_flag_accepts_split_prompt_words_for_windows_cmd_shims() { + let cli = parse_cli(&["deepseek", "-p", "hello", "world"]); + + assert_eq!(cli.prompt, vec!["hello", "world"]); + } + + #[test] + fn exec_accepts_split_prompt_words_for_windows_cmd_shims() { + let cli = parse_cli(&["deepseek", "exec", "hello", "world"]); + let Some(Commands::Exec(args)) = cli.command else { + panic!("expected exec command"); + }; + + assert_eq!(args.prompt, vec!["hello", "world"]); + } + + #[test] + fn exec_keeps_flags_before_split_prompt_words() { + let cli = parse_cli(&["deepseek", "exec", "--json", "hello", "world"]); + let Some(Commands::Exec(args)) = cli.command else { + panic!("expected exec command"); + }; + + assert!(args.json); + assert_eq!(args.prompt, vec!["hello", "world"]); + } + #[test] fn alternate_screen_defaults_on_in_auto_mode() { let cli = parse_cli(&["deepseek"]);