diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 2482f0ef..5292d2fc 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -92,6 +92,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)] @@ -517,6 +523,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(); @@ -1674,6 +1681,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() { @@ -1753,6 +1768,9 @@ 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("DEEPSEEK_VERBOSITY", v); + } if let Some(log_level) = cli.log_level.as_ref() { cmd.env("DEEPSEEK_LOG_LEVEL", log_level); } @@ -3124,6 +3142,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: std::collections::BTreeMap::new(), }; @@ -3182,6 +3201,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: std::collections::BTreeMap::new(), }; @@ -3221,6 +3241,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: std::collections::BTreeMap::new(), }; @@ -3260,6 +3281,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: resolved_headers, }; @@ -3316,6 +3338,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: std::collections::BTreeMap::new(), }; @@ -3381,6 +3404,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: std::collections::BTreeMap::new(), }; @@ -3447,6 +3471,7 @@ mod tests { approval_policy: None, sandbox_mode: None, yolo: None, + verbosity: None, http_headers: std::collections::BTreeMap::new(), }; @@ -3543,6 +3568,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 2acc34e7..aad7432b 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -565,6 +565,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, @@ -1077,6 +1078,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; } @@ -1147,6 +1151,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(), @@ -1295,6 +1300,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)?); @@ -1548,6 +1554,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, @@ -1686,6 +1693,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()); } @@ -2150,6 +2160,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, @@ -2165,6 +2180,7 @@ impl ConfigToml { approval_policy, sandbox_mode, yolo, + verbosity, http_headers, } } @@ -2770,6 +2786,7 @@ pub struct CliRuntimeOverrides { pub approval_policy: Option, pub sandbox_mode: Option, pub yolo: Option, + pub verbosity: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -2807,6 +2824,7 @@ pub struct ResolvedRuntimeOptions { pub approval_policy: Option, pub sandbox_mode: Option, pub yolo: Option, + pub verbosity: Option, pub http_headers: BTreeMap, } @@ -3237,6 +3255,7 @@ struct EnvRuntimeOverrides { approval_policy: Option, sandbox_mode: Option, yolo: Option, + verbosity: Option, http_headers: Option>, deepseek_base_url: Option, nvidia_base_url: Option, @@ -3311,6 +3330,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(), @@ -6761,4 +6783,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/src/config.rs b/crates/tui/src/config.rs index 4a27908f..862be03a 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -1638,6 +1638,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. @@ -2217,6 +2218,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!( @@ -4058,6 +4065,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); } @@ -4660,6 +4672,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), @@ -4790,7 +4803,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 6135cda8..2fa5a2c8 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. @@ -430,6 +431,7 @@ impl Default for EngineConfig { ), tools_always_load: HashSet::new(), prefer_bwrap: false, + verbosity: None, tools: None, } } @@ -700,6 +702,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); @@ -1104,6 +1107,7 @@ impl Engine { show_thinking, allowed_tools, hook_executor, + verbosity, } => { self.handle_send_message( content, @@ -1121,6 +1125,7 @@ impl Engine { show_thinking, allowed_tools, hook_executor, + verbosity, ) .await; } @@ -1378,6 +1383,7 @@ impl Engine { self.config.show_thinking, self.config.allowed_tools.clone(), self.config.hook_executor.clone(), + self.config.verbosity.clone(), ) .await; } @@ -1532,6 +1538,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(); @@ -1660,6 +1667,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. @@ -2448,6 +2456,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 4ad48c06..97b30d4f 100644 --- a/crates/tui/src/core/ops.rs +++ b/crates/tui/src/core/ops.rs @@ -41,6 +41,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 c642e152..2e9cbf5c 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -5903,6 +5903,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); @@ -5969,6 +5970,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 d713724f..ddfcd133 100644 --- a/crates/tui/src/prompts.rs +++ b/crates/tui/src/prompts.rs @@ -38,6 +38,7 @@ 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, + pub verbosity: Option<&'a str>, } impl Default for PromptSessionContext<'_> { @@ -50,6 +51,7 @@ impl Default for PromptSessionContext<'_> { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, } } } @@ -92,6 +94,18 @@ 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 translation_target_language_for_tag(locale_tag: &str) -> &'static str { let normalized = locale_tag.trim().to_ascii_lowercase(); if normalized.starts_with("ja") { @@ -1035,6 +1049,7 @@ pub fn system_prompt_for_mode_with_context_and_skills( translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) } @@ -1117,6 +1132,13 @@ pub fn system_prompt_for_mode_with_context_skills_session_and_approval( ); } + if session_context.verbosity == Some("concise") { + 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 @@ -1909,6 +1931,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -1978,6 +2001,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2020,6 +2044,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: false, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2072,6 +2097,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2175,6 +2201,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2211,6 +2238,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2239,6 +2267,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2296,6 +2325,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2324,6 +2354,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2530,6 +2561,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -2564,6 +2596,7 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, + verbosity: None, }, ) { SystemPrompt::Text(text) => text, @@ -3088,4 +3121,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 724c9d35..54399cc5 100644 --- a/crates/tui/src/runtime_threads.rs +++ b/crates/tui/src/runtime_threads.rs @@ -1708,6 +1708,7 @@ impl RuntimeThreadManager { } else { crate::tui::approval::ApprovalMode::Suggest }, + verbosity: self.config.verbosity.clone(), }) .await .map_err(|e| anyhow!("Failed to start turn: {e}"))?; @@ -2093,6 +2094,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 920584cc..762517f9 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -1438,6 +1438,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, @@ -2203,6 +2204,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 350265b4..f699023d 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, @@ -5516,6 +5517,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(), }, ), ); @@ -5618,6 +5620,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 {