diff --git a/CHANGELOG.md b/CHANGELOG.md index 88dfe3c9..7a98906d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 providers now report whether the value came from `--provider`, environment, or config. Config-sourced unsupported providers fall back to DeepSeek without forwarding stale keyring secrets. Thanks @cyq1017 for the PR. +- **Exec auto-model handoff (#3148).** `codewhale exec --model auto` now + survives the CLI/TUI boundary by honoring the CodeWhale model env alias and + legacy DeepSeek model handoff before falling back to provider defaults. + Thanks @hongchen1993 for the PR. - **TUI mouse-report leak (#3063/#3067).** Strip raw SGR mouse coordinate tails from the composer even when `use_mouse_capture` is false, covering orphaned terminal reporting state after crashes or focus races. diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 1949f190..f43019b0 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -46,6 +46,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 providers now report whether the value came from `--provider`, environment, or config. Config-sourced unsupported providers fall back to DeepSeek without forwarding stale keyring secrets. Thanks @cyq1017 for the PR. +- **Exec auto-model handoff (#3148).** `codewhale exec --model auto` now + survives the CLI/TUI boundary by honoring the CodeWhale model env alias and + legacy DeepSeek model handoff before falling back to provider defaults. + Thanks @hongchen1993 for the PR. - **TUI mouse-report leak (#3063/#3067).** Strip raw SGR mouse coordinate tails from the composer even when `use_mouse_capture` is false, covering orphaned terminal reporting state after crashes or focus races. diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index bfb7b2db..93108c10 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -307,14 +307,6 @@ Plain `codewhale exec` is a one-shot model response. Use `--auto` for non-interactive filesystem/shell tool use. ")] struct ExecArgs { - /// Prompt to send to the model - #[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, @@ -349,6 +341,14 @@ struct ExecArgs { /// Extra text appended to the system prompt for this run. #[arg(long)] append_system_prompt: Option, + /// Prompt to send to the model + #[arg( + value_name = "PROMPT", + required = true, + trailing_var_arg = true, + allow_hyphen_values = true + )] + prompt: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] @@ -464,9 +464,21 @@ fn resolve_exec_model(config: &Config, explicit_model: Option<&str>) -> String { .map(str::trim) .filter(|model| !model.is_empty()) .map(ToOwned::to_owned) + .or_else(exec_model_env_override) .unwrap_or_else(|| config.default_model()) } +fn exec_model_env_override() -> Option { + ["CODEWHALE_MODEL", "DEEPSEEK_MODEL"] + .into_iter() + .find_map(|key| { + std::env::var(key) + .ok() + .map(|model| model.trim().to_string()) + .filter(|model| !model.is_empty()) + }) +} + fn top_level_prompt_initial_input(parts: &[String]) -> Option { (!parts.is_empty()).then(|| tui::InitialInput::Submit(join_prompt_parts(parts))) } @@ -6647,6 +6659,9 @@ mod terminal_mode_tests { #[test] fn exec_model_resolution_uses_provider_scoped_default() { + let _env_lock = crate::test_support::lock_test_env(); + let _codewhale_model = crate::test_support::EnvVarGuard::remove("CODEWHALE_MODEL"); + let _deepseek_model = crate::test_support::EnvVarGuard::remove("DEEPSEEK_MODEL"); let config = Config { provider: Some("openrouter".to_string()), default_text_model: Some("deepseek/deepseek-v4-pro".to_string()), @@ -6670,6 +6685,33 @@ mod terminal_mode_tests { ); } + #[test] + fn exec_model_resolution_prefers_codewhale_model_env_override() { + let _env_lock = crate::test_support::lock_test_env(); + let _codewhale_model = crate::test_support::EnvVarGuard::set("CODEWHALE_MODEL", " auto "); + let _deepseek_model = + crate::test_support::EnvVarGuard::set("DEEPSEEK_MODEL", "stale-deepseek-model"); + let config = Config { + default_text_model: Some("deepseek/deepseek-v4-pro".to_string()), + ..Default::default() + }; + + assert_eq!(resolve_exec_model(&config, None), "auto"); + } + + #[test] + fn exec_model_resolution_uses_legacy_deepseek_model_env_override() { + let _env_lock = crate::test_support::lock_test_env(); + let _codewhale_model = crate::test_support::EnvVarGuard::remove("CODEWHALE_MODEL"); + let _deepseek_model = crate::test_support::EnvVarGuard::set("DEEPSEEK_MODEL", " auto "); + let config = Config { + default_text_model: Some("deepseek/deepseek-v4-pro".to_string()), + ..Default::default() + }; + + assert_eq!(resolve_exec_model(&config, None), "auto"); + } + #[test] fn exec_accepts_split_prompt_words_for_windows_cmd_shims() { let cli = parse_cli(&["codewhale", "exec", "hello", "world"]); @@ -6680,6 +6722,17 @@ mod terminal_mode_tests { assert_eq!(args.prompt, vec!["hello", "world"]); } + #[test] + fn exec_keeps_model_flag_before_split_prompt_words() { + let cli = parse_cli(&["codewhale", "exec", "--model", "auto", "hello", "world"]); + let Some(Commands::Exec(args)) = cli.command else { + panic!("expected exec command"); + }; + + assert_eq!(args.model.as_deref(), Some("auto")); + assert_eq!(args.prompt, vec!["hello", "world"]); + } + #[test] fn exec_keeps_flags_before_split_prompt_words() { let cli = parse_cli(&["codewhale", "exec", "--json", "hello", "world"]);