From 42de833d80be203a1617ecfbd9b36931340cbe9f Mon Sep 17 00:00:00 2001 From: cyq1017 <61975706+cyq1017@users.noreply.github.com> Date: Fri, 12 Jun 2026 06:41:01 -0700 Subject: [PATCH] feat(config): add concise verbosity mode Harvests #3052 onto the v0.8.59 release branch. Noninteractive CLI launches default to concise output discipline unless config, env, or --verbosity overrides it; interactive TUI launches remain normal by default. Also forwards CODEWHALE_VERBOSITY as the primary env bridge while keeping DEEPSEEK_VERBOSITY for compatibility, documents the setting, and records @cyq1017 credit in both changelogs. --- CHANGELOG.md | 4 ++ crates/cli/src/lib.rs | 78 +++++++++++++++++++++++++++ crates/config/src/lib.rs | 54 +++++++++++++++++++ crates/tui/CHANGELOG.md | 4 ++ crates/tui/src/config.rs | 20 ++++++- crates/tui/src/core/engine.rs | 9 ++++ crates/tui/src/core/ops.rs | 1 + crates/tui/src/main.rs | 2 + crates/tui/src/prompts.rs | 69 ++++++++++++++++++++++++ crates/tui/src/prompts/modes/agent.md | 2 + crates/tui/src/prompts/modes/plan.md | 2 + crates/tui/src/prompts/modes/yolo.md | 2 + crates/tui/src/runtime_threads.rs | 2 + crates/tui/src/tui/app.rs | 2 + crates/tui/src/tui/ui.rs | 3 ++ docs/CONFIGURATION.md | 7 +++ 16 files changed, 260 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e8aa77..9bda530f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `kimi-k2.7-code`, recognizes `kimi`/`kimi-k2` aliases for that model, keeps explicit `kimi-k2.6` selectable, and adds the OpenRouter `moonshotai/kimi-k2.7-code` registry row. +- **Concise verbosity mode (#3052).** CLI noninteractive launches now default + to concise prompt/output discipline unless overridden by config, env, or + `--verbosity`, while interactive TUI launches remain normal by default. + Thanks @cyq1017 for the PR. - **Ephemeral generated project context (#3058).** Opening CodeWhale in a directory with no instruction files now keeps the bounded generated project overview in memory instead of creating `.codewhale/instructions.md`. diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 93c2c996..fcf07e90 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -93,6 +93,12 @@ struct Cli { model: Option, #[arg(long = "output-mode")] output_mode: Option, + #[arg( + long = "verbosity", + value_name = "LEVEL", + help = "Controls transcript and output verbosity (normal, concise)" + )] + verbosity: Option, #[arg(long = "log-level")] log_level: Option, #[arg(long)] @@ -518,6 +524,7 @@ fn run() -> Result<()> { approval_policy: cli.approval_policy.clone(), sandbox_mode: cli.sandbox_mode.clone(), yolo: Some(cli.yolo), + verbosity: cli.verbosity.clone(), }; let command = cli.command.take(); @@ -1685,6 +1692,14 @@ fn build_tui_command( passthrough: Vec, ) -> Result { let tui = locate_sibling_tui_binary()?; + let mut verbosity = resolved_runtime.verbosity.clone(); + if verbosity.is_none() + && passthrough + .iter() + .any(|arg| matches!(arg.as_str(), "exec" | "swebench" | "eval")) + { + verbosity = Some("concise".to_string()); + } let mut cmd = Command::new(&tui); if let Some(config) = cli.config.as_ref() { @@ -1790,6 +1805,10 @@ fn build_tui_command( if let Some(output_mode) = cli.output_mode.as_ref() { cmd.env("DEEPSEEK_OUTPUT_MODE", output_mode); } + if let Some(v) = verbosity.as_ref() { + cmd.env("CODEWHALE_VERBOSITY", v); + cmd.env("DEEPSEEK_VERBOSITY", v); + } if let Some(log_level) = cli.log_level.as_ref() { cmd.env("DEEPSEEK_LOG_LEVEL", log_level); } @@ -2052,6 +2071,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: std::collections::BTreeMap::new(), } } @@ -3120,6 +3140,8 @@ mod tests { "deepseek-v4-pro", "--output-mode", "json", + "--verbosity", + "concise", "--log-level", "debug", "--telemetry", @@ -3147,6 +3169,7 @@ mod tests { assert_eq!(cli.profile.as_deref(), Some("work")); assert_eq!(cli.model.as_deref(), Some("deepseek-v4-pro")); assert_eq!(cli.output_mode.as_deref(), Some("json")); + assert_eq!(cli.verbosity.as_deref(), Some("concise")); assert_eq!(cli.log_level.as_deref(), Some("debug")); assert_eq!(cli.telemetry, Some(true)); assert_eq!(cli.approval_policy.as_deref(), Some("on-request")); @@ -3196,6 +3219,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: std::collections::BTreeMap::new(), }; @@ -3255,6 +3279,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: std::collections::BTreeMap::new(), }; @@ -3295,6 +3320,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: std::collections::BTreeMap::new(), }; @@ -3395,6 +3421,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: resolved_headers, }; @@ -3417,6 +3444,53 @@ mod tests { ); } + #[test] + fn build_tui_command_defaults_noninteractive_to_concise_verbosity() { + let _lock = env_lock(); + let (_dir, _bin) = install_fake_tui_binary(); + + let cli = parse_ok(&["codewhale"]); + let resolved = resolved_runtime_for_test(ProviderKind::Deepseek, ProviderSource::Config); + + let cmd = build_tui_command( + &cli, + &resolved, + vec!["exec".to_string(), "summarize".to_string()], + ) + .expect("command"); + + assert_eq!( + command_env(&cmd, "CODEWHALE_VERBOSITY").as_deref(), + Some("concise") + ); + assert_eq!( + command_env(&cmd, "DEEPSEEK_VERBOSITY").as_deref(), + Some("concise") + ); + } + + #[test] + fn build_tui_command_respects_resolved_verbosity_override() { + let _lock = env_lock(); + let (_dir, _bin) = install_fake_tui_binary(); + + let cli = parse_ok(&["codewhale"]); + let mut resolved = + resolved_runtime_for_test(ProviderKind::Deepseek, ProviderSource::Config); + resolved.verbosity = Some("normal".to_string()); + + let cmd = build_tui_command(&cli, &resolved, vec!["exec".to_string()]).expect("command"); + + assert_eq!( + command_env(&cmd, "CODEWHALE_VERBOSITY").as_deref(), + Some("normal") + ); + assert_eq!( + command_env(&cmd, "DEEPSEEK_VERBOSITY").as_deref(), + Some("normal") + ); + } + #[test] fn build_tui_command_allows_moonshot_and_forwards_kimi_key() { let _lock = env_lock(); @@ -3452,6 +3526,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: std::collections::BTreeMap::new(), }; @@ -3518,6 +3593,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: std::collections::BTreeMap::new(), }; @@ -3585,6 +3661,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: std::collections::BTreeMap::new(), }; @@ -3682,6 +3759,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: std::collections::BTreeMap::new(), }; diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index aad4881b..e50f1e1a 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -567,6 +567,7 @@ pub struct ConfigToml { pub model: Option, pub auth_mode: Option, pub output_mode: Option, + pub verbosity: Option, pub log_level: Option, pub telemetry: Option, pub approval_policy: Option, @@ -1079,6 +1080,9 @@ impl ConfigToml { if project.output_mode.is_some() { self.output_mode = project.output_mode; } + if project.verbosity.is_some() { + self.verbosity = project.verbosity; + } if project.log_level.is_some() { self.log_level = project.log_level; } @@ -1153,6 +1157,7 @@ impl ConfigToml { "model" => self.model.clone(), "auth.mode" => self.auth_mode.clone(), "output_mode" => self.output_mode.clone(), + "verbosity" => self.verbosity.clone(), "log_level" => self.log_level.clone(), "telemetry" => self.telemetry.map(|v| v.to_string()), "approval_policy" => self.approval_policy.clone(), @@ -1307,6 +1312,7 @@ impl ConfigToml { "model" => self.model = Some(value.to_string()), "auth.mode" => self.auth_mode = Some(value.to_string()), "output_mode" => self.output_mode = Some(value.to_string()), + "verbosity" => self.verbosity = Some(value.to_string()), "log_level" => self.log_level = Some(value.to_string()), "telemetry" => { self.telemetry = Some(parse_bool(value)?); @@ -1572,6 +1578,7 @@ impl ConfigToml { "model" => self.model = None, "auth.mode" => self.auth_mode = None, "output_mode" => self.output_mode = None, + "verbosity" => self.verbosity = None, "log_level" => self.log_level = None, "telemetry" => self.telemetry = None, "approval_policy" => self.approval_policy = None, @@ -1716,6 +1723,9 @@ impl ConfigToml { if let Some(v) = self.output_mode.as_ref() { out.insert("output_mode".to_string(), v.clone()); } + if let Some(v) = self.verbosity.as_ref() { + out.insert("verbosity".to_string(), v.clone()); + } if let Some(v) = self.log_level.as_ref() { out.insert("log_level".to_string(), v.clone()); } @@ -2204,6 +2214,11 @@ impl ConfigToml { .or_else(|| env.sandbox_mode.clone()) .or_else(|| self.sandbox_mode.clone()); let yolo = cli.yolo.or(env.yolo); + let verbosity = cli + .verbosity + .clone() + .or_else(|| env.verbosity.clone()) + .or_else(|| self.verbosity.clone()); ResolvedRuntimeOptions { provider, @@ -2220,6 +2235,7 @@ impl ConfigToml { approval_policy, sandbox_mode, yolo, + verbosity, http_headers, } } @@ -2848,6 +2864,7 @@ pub struct CliRuntimeOverrides { pub approval_policy: Option, pub sandbox_mode: Option, pub yolo: Option, + pub verbosity: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -2893,6 +2910,7 @@ pub struct ResolvedRuntimeOptions { pub approval_policy: Option, pub sandbox_mode: Option, pub yolo: Option, + pub verbosity: Option, pub http_headers: BTreeMap, } @@ -3324,6 +3342,7 @@ struct EnvRuntimeOverrides { approval_policy: Option, sandbox_mode: Option, yolo: Option, + verbosity: Option, http_headers: Option>, deepseek_base_url: Option, nvidia_base_url: Option, @@ -3397,6 +3416,9 @@ impl EnvRuntimeOverrides { arcee_model: std::env::var("ARCEE_MODEL") .ok() .filter(|v| !v.trim().is_empty()), + verbosity: std::env::var("CODEWHALE_VERBOSITY") + .or_else(|_| std::env::var("DEEPSEEK_VERBOSITY")) + .ok(), output_mode: std::env::var("DEEPSEEK_OUTPUT_MODE").ok(), auth_mode: std::env::var("DEEPSEEK_AUTH_MODE").ok(), log_level: std::env::var("DEEPSEEK_LOG_LEVEL").ok(), @@ -7022,4 +7044,36 @@ unknown_policy = "surprise" assert!(err.to_string().contains("unknown_policy")); } + + #[test] + fn test_verbosity_resolution() { + let _lock = env_lock(); + // Test TOML parsing + let toml_str = r#" + verbosity = "concise" + "#; + let config: ConfigToml = toml::from_str(toml_str).unwrap(); + assert_eq!(config.verbosity, Some("concise".to_string())); + + // Test Env overrides + let _env = EnvGuard::without_deepseek_runtime_overrides(); + unsafe { + std::env::set_var("CODEWHALE_VERBOSITY", "normal"); + } + let env_overrides = EnvRuntimeOverrides::load(); + assert_eq!(env_overrides.verbosity, Some("normal".to_string())); + unsafe { + std::env::remove_var("CODEWHALE_VERBOSITY"); + } + + // Test fallback to DEEPSEEK_VERBOSITY + unsafe { + std::env::set_var("DEEPSEEK_VERBOSITY", "concise"); + } + let env_overrides = EnvRuntimeOverrides::load(); + assert_eq!(env_overrides.verbosity, Some("concise".to_string())); + unsafe { + std::env::remove_var("DEEPSEEK_VERBOSITY"); + } + } } diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 5165f8f8..058bcc99 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `kimi-k2.7-code`, recognizes `kimi`/`kimi-k2` aliases for that model, keeps explicit `kimi-k2.6` selectable, and adds the OpenRouter `moonshotai/kimi-k2.7-code` registry row. +- **Concise verbosity mode (#3052).** CLI noninteractive launches now default + to concise prompt/output discipline unless overridden by config, env, or + `--verbosity`, while interactive TUI launches remain normal by default. + Thanks @cyq1017 for the PR. - **Ephemeral generated project context (#3058).** Opening CodeWhale in a directory with no instruction files now keeps the bounded generated project overview in memory instead of creating `.codewhale/instructions.md`. diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index ef26af6e..2e0dfdb1 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -1707,6 +1707,7 @@ pub struct Config { pub approval_policy: Option, pub sandbox_mode: Option, pub yolo: Option, + pub verbosity: Option, /// External sandbox backend: `"none"` or `"opensandbox"`. /// When set, exec_shell routes commands through the backend's HTTP API /// instead of spawning a local process. @@ -2287,6 +2288,12 @@ impl Config { ); } } + if let Some(v) = self.verbosity.as_deref() { + let normalized = v.trim().to_ascii_lowercase(); + if !matches!(normalized.as_str(), "normal" | "concise") { + anyhow::bail!("Invalid verbosity '{v}': expected normal or concise."); + } + } if let Some(mode) = self.sandbox_mode.as_deref() { let normalized = mode.trim().to_ascii_lowercase(); if !matches!( @@ -4164,6 +4171,11 @@ fn apply_env_overrides(config: &mut Config) { if let Ok(value) = std::env::var("DEEPSEEK_YOLO") { config.yolo = Some(value == "1" || value.eq_ignore_ascii_case("true")); } + if let Ok(value) = + std::env::var("CODEWHALE_VERBOSITY").or_else(|_| std::env::var("DEEPSEEK_VERBOSITY")) + { + config.verbosity = Some(value); + } if let Ok(value) = std::env::var("DEEPSEEK_SANDBOX_BACKEND") { config.sandbox_backend = Some(value); } @@ -4790,6 +4802,7 @@ fn merge_config(base: Config, override_cfg: Config) -> Config { allow_shell: override_cfg.allow_shell.or(base.allow_shell), prompt_suggestion: override_cfg.prompt_suggestion.or(base.prompt_suggestion), yolo: override_cfg.yolo.or(base.yolo), + verbosity: override_cfg.verbosity.or(base.verbosity), approval_policy: override_cfg.approval_policy.or(base.approval_policy), sandbox_mode: override_cfg.sandbox_mode.or(base.sandbox_mode), sandbox_backend: override_cfg.sandbox_backend.or(base.sandbox_backend), @@ -4920,7 +4933,12 @@ fn warn_on_misplaced_top_level_keys(raw: &str) -> Option { // Sections CodeWhale does not recognize but users nest settings under. const UNKNOWN_SECTIONS: &[&str] = &["general", "sandbox"]; // Keys that are only ever read from the top level of the config. - const TOP_LEVEL_KEYS: &[&str] = &["allow_shell", "sandbox_mode", "approval_policy"]; + const TOP_LEVEL_KEYS: &[&str] = &[ + "allow_shell", + "sandbox_mode", + "approval_policy", + "verbosity", + ]; let mut hits: Vec = Vec::new(); for section in UNKNOWN_SECTIONS { diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index 9db1130c..3f3ac286 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -274,6 +274,7 @@ pub struct EngineConfig { /// Whether user-visible transcript rendering shows thinking blocks. /// Prompt assembly uses this to avoid localizing hidden reasoning. pub show_thinking: bool, + pub verbosity: Option, /// Maximum number of assistant steps before stopping. pub max_steps: u32, /// Maximum number of concurrently active subagents. @@ -439,6 +440,7 @@ impl Default for EngineConfig { ), tools_always_load: HashSet::new(), prefer_bwrap: false, + verbosity: None, tools: None, } } @@ -714,6 +716,7 @@ impl Engine { translation_enabled: config.translation_enabled, model_id: &config.model, show_thinking: config.show_thinking, + verbosity: config.verbosity.as_deref(), }, ); let stable_prompt = Some(system_prompt); @@ -1121,6 +1124,7 @@ impl Engine { show_thinking, allowed_tools, hook_executor, + verbosity, } => { self.handle_send_message( content, @@ -1140,6 +1144,7 @@ impl Engine { show_thinking, allowed_tools, hook_executor, + verbosity, ) .await; } @@ -1399,6 +1404,7 @@ impl Engine { self.config.show_thinking, self.config.allowed_tools.clone(), self.config.hook_executor.clone(), + self.config.verbosity.clone(), ) .await; } @@ -1574,6 +1580,7 @@ impl Engine { show_thinking: bool, allowed_tools: Option>, hook_executor: Option>, + verbosity: Option, ) { // Reset cancel token for fresh turn (in case previous was cancelled) self.reset_cancel_token(); @@ -1708,6 +1715,7 @@ impl Engine { self.config.trust_mode = trust_mode; self.config.translation_enabled = translation_enabled; self.config.show_thinking = show_thinking; + self.config.verbosity = verbosity; // Refresh stable prompt context. Current mode is carried by the // request-time runtime prompt projection. @@ -2497,6 +2505,7 @@ impl Engine { translation_enabled: self.config.translation_enabled, model_id: &self.config.model, show_thinking: self.config.show_thinking, + verbosity: self.config.verbosity.as_deref(), }, ); let mut stable_prompt = diff --git a/crates/tui/src/core/ops.rs b/crates/tui/src/core/ops.rs index 7738f9dd..547b88e3 100644 --- a/crates/tui/src/core/ops.rs +++ b/crates/tui/src/core/ops.rs @@ -44,6 +44,7 @@ pub enum Op { /// Hook executor for control-plane hooks. /// `ToolCallBefore` hooks may deny a tool call with exit code 2. hook_executor: Option>, + verbosity: Option, }, /// Execute a user-submitted composer shell command (`! `) without diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index a34c4277..399ccc76 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -5938,6 +5938,7 @@ async fn run_exec_agent( search_base_url: config.search.as_ref().and_then(|s| s.base_url.clone()), tools_always_load: config.tools_always_load(), tools: config.tools.clone(), + verbosity: config.verbosity.clone(), }; let engine_handle = spawn_engine(engine_config, config); @@ -6006,6 +6007,7 @@ async fn run_exec_agent( .and_then(crate::tui::approval::ApprovalMode::from_config_value) .unwrap_or_default() }, + verbosity: config.verbosity.clone(), }) .await?; diff --git a/crates/tui/src/prompts.rs b/crates/tui/src/prompts.rs index a1a69f2b..0b4b0e06 100644 --- a/crates/tui/src/prompts.rs +++ b/crates/tui/src/prompts.rs @@ -38,6 +38,9 @@ pub struct PromptSessionContext<'a> { /// When false, the prompt should not spend localization pressure on /// `reasoning_content` the user will never see. pub show_thinking: bool, + /// Optional output-verbosity mode. `concise` appends a short output + /// discipline block; unset keeps the normal conversational prompt. + pub verbosity: Option<&'a str>, } impl Default for PromptSessionContext<'_> { @@ -50,6 +53,7 @@ impl Default for PromptSessionContext<'_> { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, } } } @@ -92,6 +96,22 @@ so any English prose in your response will block their decision-making." ) } +fn concise_output_discipline_instruction() -> &'static str { + "\ +## Concise Output Discipline + +To minimize token usage and optimize speed: +- Output only direct, actionable code, technical steps, or final answers. +- Eliminate all conversational filler, fluff, introductions, transitions, or summarizing conclusions. +- Do NOT explain what you are about to do or what you have just completed. +- Do NOT provide conversational status updates before or after running tools. +- Keep explanations and comments extremely brief and technical, explaining only non-obvious reasoning." +} + +fn is_concise_verbosity(value: Option<&str>) -> bool { + value.is_some_and(|v| v.trim().eq_ignore_ascii_case("concise")) +} + fn translation_target_language_for_tag(locale_tag: &str) -> &'static str { let normalized = locale_tag.trim().to_ascii_lowercase(); if normalized.starts_with("ja") { @@ -1040,6 +1060,7 @@ pub fn system_prompt_for_mode_with_context_and_skills( translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) } @@ -1122,6 +1143,13 @@ pub fn system_prompt_for_mode_with_context_skills_session_and_approval( ); } + if is_concise_verbosity(session_context.verbosity) { + full_prompt = format!( + "{full_prompt}\n\n{}", + concise_output_discipline_instruction() + ); + } + // 3. Skills block. #432: walks every candidate workspace // skills directory (`.agents/skills`, `skills`, // `.opencode/skills`, `.claude/skills`, `.cursor/skills`) plus global @@ -1934,6 +1962,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2003,6 +2032,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2045,6 +2075,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: false, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2097,6 +2128,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2200,6 +2232,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2236,6 +2269,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2264,6 +2298,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2321,6 +2356,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2349,6 +2385,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2555,6 +2592,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2589,6 +2627,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -3113,4 +3152,34 @@ mod tests { "instructions block must annotate its source path" ); } + + #[test] + fn verbosity_concise_appends_discipline_block() { + let tmp = tempdir().expect("tempdir"); + let workspace = tmp.path(); + let prompt = match super::system_prompt_for_mode_with_context_skills_session_and_approval( + workspace, + None, + None, + None, + PromptSessionContext { + user_memory_block: None, + goal_objective: None, + project_context_pack_enabled: false, + locale_tag: "en", + translation_enabled: false, + model_id: "codewhale", + show_thinking: true, + verbosity: Some(" Concise "), + }, + ) { + SystemPrompt::Text(text) => text, + SystemPrompt::Blocks(_) => panic!("expected text system prompt"), + }; + + assert!( + prompt.contains("## Concise Output Discipline"), + "Concise Output Discipline should be appended" + ); + } } diff --git a/crates/tui/src/prompts/modes/agent.md b/crates/tui/src/prompts/modes/agent.md index 38ae028c..8b0f56de 100644 --- a/crates/tui/src/prompts/modes/agent.md +++ b/crates/tui/src/prompts/modes/agent.md @@ -29,3 +29,5 @@ Long sessions accumulate context. To stay fast: - Suggest `/compact` or Ctrl+L when context nears 60% during sustained work — the compaction relay preserves open blockers - Use `note` for decisions you'll need across compaction boundaries - A 3-turn session that fans out to sub-agents finishes faster AND stays responsive longer than a 15-turn sequential grind + +Do NOT explain, announce, or mention to the user that you are running in Agent mode or how the approval policy works. Act silently on this mode instruction. diff --git a/crates/tui/src/prompts/modes/plan.md b/crates/tui/src/prompts/modes/plan.md index 3e6e648b..c4c80740 100644 --- a/crates/tui/src/prompts/modes/plan.md +++ b/crates/tui/src/prompts/modes/plan.md @@ -15,3 +15,5 @@ can't change it. Shell and code execution are unavailable. Use this mode to build a thorough plan. Spawn read-only sub-agents for parallel investigation. After `update_plan` presents the plan, wait for the user's next action instead of continuing to tool around in Plan mode. + +Do NOT explain, announce, or mention to the user that you are running in Plan mode, or describe the transition. Act silently on this mode instruction. diff --git a/crates/tui/src/prompts/modes/yolo.md b/crates/tui/src/prompts/modes/yolo.md index 0e867fb5..28456b6d 100644 --- a/crates/tui/src/prompts/modes/yolo.md +++ b/crates/tui/src/prompts/modes/yolo.md @@ -9,3 +9,5 @@ Even with auto-approval, use `checklist_write` for work that has several concret visible and trackable in the sidebar. Keep simple commands and focused edits direct. For multi-step initiatives, keep `checklist_write` current. Add `update_plan` only when a high-level strategy would help and do not duplicate the checklist there. + +Do NOT announce or mention to the user that you are running in YOLO mode. Act silently on this mode instruction. diff --git a/crates/tui/src/runtime_threads.rs b/crates/tui/src/runtime_threads.rs index 3039ad18..d5f2db3f 100644 --- a/crates/tui/src/runtime_threads.rs +++ b/crates/tui/src/runtime_threads.rs @@ -1763,6 +1763,7 @@ impl RuntimeThreadManager { } else { crate::tui::approval::ApprovalMode::Suggest }, + verbosity: self.config.verbosity.clone(), }) .await .map_err(|e| anyhow!("Failed to start turn: {e}"))?; @@ -2151,6 +2152,7 @@ impl RuntimeThreadManager { search_base_url: self.config.search.as_ref().and_then(|s| s.base_url.clone()), tools_always_load: self.config.tools_always_load(), tools: self.config.tools.clone(), + verbosity: self.config.verbosity.clone(), }; let engine = spawn_engine(engine_cfg, &self.config); diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index 74e3c19b..f75a2d3a 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -1519,6 +1519,7 @@ pub struct App { pub compact_threshold: usize, pub max_input_history: usize, pub allow_shell: bool, + pub verbosity: Option, pub max_subagents: usize, /// Per-SSE-chunk idle timeout for streamed turns, in seconds. pub stream_chunk_timeout_secs: u64, @@ -2285,6 +2286,7 @@ impl App { compact_threshold, max_input_history, allow_shell, + verbosity: config.verbosity.clone(), max_subagents, stream_chunk_timeout_secs: config.stream_chunk_timeout_secs(), subagent_cache: Vec::new(), diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index db4250e1..2c3e4141 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -873,6 +873,7 @@ fn build_engine_config(app: &App, config: &Config) -> EngineConfig { project_context_pack_enabled: config.project_context_pack_enabled(), translation_enabled: app.translation_enabled, show_thinking: app.show_thinking, + verbosity: app.verbosity.clone(), // Effectively unlimited. V4 has a 1M context window and the user // wants the model running until it's actually done. The previous cap // of 100 hit the ceiling on long multi-step plans (wide refactors, @@ -5531,6 +5532,7 @@ async fn dispatch_user_message( translation_enabled: app.translation_enabled, model_id: &app.model, show_thinking: app.show_thinking, + verbosity: app.verbosity.as_deref(), }, ), ); @@ -5642,6 +5644,7 @@ async fn dispatch_user_message( show_thinking: app.show_thinking, allowed_tools: app.active_allowed_tools.clone(), hook_executor: app.runtime_services.hook_executor.clone(), + verbosity: app.verbosity.clone(), }) .await { diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 8d57ba10..0a527f0d 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -116,6 +116,7 @@ Supported keys in the project overlay (top-level fields only): | `api_key` | use a per-repo key (typically read from `.env`, **not committed**) | | `base_url` | point at a self-hosted endpoint | | `reasoning_effort` | force `"high"` / `"max"` for a complex repo | +| `verbosity` | use `"normal"` or `"concise"` prompt/output discipline | | `approval_policy` | `"never"` / `"on-request"` / `"untrusted"` for opinionated repos | | `sandbox_mode` | `"read-only"` / `"workspace-write"` / `"danger-full-access"` | | `mcp_config_path` | per-repo MCP server set | @@ -958,6 +959,12 @@ If you are upgrading from older releases: - `insecure_skip_tls_verify` (bool, optional provider-table key): disabled by default. When true on the active provider table, only the LLM provider HTTP client skips TLS certificate verification. Prefer `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports this setting when enabled. - `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek and generic OpenAI-compatible endpoints, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.5-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, and `deepseek-coder:1.3b` for Ollama. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`. Current public DeepSeek IDs are `deepseek-v4-pro` and `deepseek-v4-flash`, both with 1M context windows, 384K max output, and thinking mode enabled by default. Legacy `deepseek-chat` and `deepseek-reasoner` remain compatibility aliases for `deepseek-v4-flash` until July 24, 2026, except SiliconFlow maps `deepseek-reasoner` and `deepseek-r1` to its Pro model while `deepseek-chat` and `deepseek-v3` map to Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k2.7-code` and `kimi-k2.6`, with `kimi` and `kimi-k2` aliases selecting `kimi-k2.7-code`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.5-pro` and `mimo-v2.5`, while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, and Ollama model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias. - `reasoning_effort` (string, optional): `off`, `low`, `medium`, `high`, `max`, or `xhigh`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. OpenAI Codex normalizes stale `off` to `low` and sends `max` as Responses `xhigh`. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`. +- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the + default conversational prompt. `concise` appends a prompt discipline block + for direct, low-chatter output; CLI noninteractive commands (`exec`, `eval`, + and `swebench`) default to `concise` unless config/env/CLI overrides it. + Override per process with `CODEWHALE_VERBOSITY` or the legacy + `DEEPSEEK_VERBOSITY` alias. - `allow_shell` (bool, optional): defaults to `false`; shell tools must be explicitly enabled. - `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases. - `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.