From 5e7224900e208b69f99a21e53f2dcb45b22642a3 Mon Sep 17 00:00:00 2001 From: Som Date: Wed, 19 Aug 2026 19:38:39 +0000 Subject: [PATCH 1/4] fix(config): warn when common config disables Claude Monitor traffic Setting CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC in a Claude common-config snippet silently disables Claude Code's Monitor tool. Detect it in the CLI (config common set / extract --save) and TUI common-config editor and surface a non-blocking warning instead of applying it silently. Fixes #184 --- src-tauri/src/cli/commands/config_common.rs | 127 ++++++++++++++- src-tauri/src/cli/commands/mod.rs | 2 +- src-tauri/src/cli/i18n.rs | 8 + .../src/cli/tui/runtime_actions/editor.rs | 145 +++++++++++++++++- 4 files changed, 279 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/cli/commands/config_common.rs b/src-tauri/src/cli/commands/config_common.rs index b06e93a8..cb680900 100644 --- a/src-tauri/src/cli/commands/config_common.rs +++ b/src-tauri/src/cli/commands/config_common.rs @@ -4,7 +4,7 @@ use std::path::Path; use crate::app_config::AppType; use crate::cli::i18n::texts; -use crate::cli::ui::{highlight, info, success}; +use crate::cli::ui::{highlight, info, success, warning}; use crate::error::AppError; use crate::services::ProviderService; use crate::store::AppState; @@ -207,6 +207,36 @@ fn canonical_common_snippet(app_type: AppType, raw: &str) -> Result bool { + if *app_type != AppType::Claude { + return false; + } + + let Ok(value) = serde_json::from_str::(canonical_snippet) else { + return false; + }; + + let Some(raw) = value.get("env").and_then(|env| { + env.get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC") + }) else { + return false; + }; + + match raw { + serde_json::Value::Number(n) => n.as_i64() == Some(1), + serde_json::Value::Bool(b) => *b, + serde_json::Value::String(s) => s == "1" || s == "true", + _ => false, + } +} + fn format( app_type: AppType, snippet_text: Option<&str>, @@ -235,6 +265,8 @@ fn set( texts::config_common_snippet_require_json_or_file(), )?; let snippet = canonical_common_snippet(app_type.clone(), &raw)?.unwrap_or_default(); + let disables_monitor_traffic = + common_config_snippet_disables_monitor_traffic(&app_type, &snippet); let state = get_state()?; ProviderService::set_common_config_snippet(&state, app_type.clone(), Some(snippet))?; @@ -243,6 +275,12 @@ fn set( "{}", success(&texts::config_common_snippet_set_for_app(app_type.as_str())) ); + if disables_monitor_traffic { + println!( + "{}", + warning(texts::common_config_snippet_disables_monitor_traffic()) + ); + } let current_id = if app_type.is_additive_mode() { String::new() @@ -296,12 +334,20 @@ fn extract( if save { let snippet = canonical_common_snippet(app_type.clone(), &extracted)?.unwrap_or_default(); + let disables_monitor_traffic = + common_config_snippet_disables_monitor_traffic(&app_type, &snippet); ProviderService::set_common_config_snippet( &state, app_type.clone(), Some(snippet.clone()), )?; println!("{}", success(texts::common_config_snippet_extracted())); + if disables_monitor_traffic { + println!( + "{}", + warning(texts::common_config_snippet_disables_monitor_traffic()) + ); + } if !snippet.trim().is_empty() { println!(); println!("{}", snippet); @@ -431,6 +477,55 @@ mod tests { seed_current_codex_provider_with_meta(None) } + #[test] + fn disables_monitor_traffic_detects_truthy_values() { + for truthy in [ + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":1}}"#, + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":true}}"#, + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1"}}"#, + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"true"}}"#, + ] { + assert!( + common_config_snippet_disables_monitor_traffic(&AppType::Claude, truthy), + "expected truthy detection for snippet: {truthy}" + ); + } + } + + #[test] + fn disables_monitor_traffic_ignores_falsy_or_absent_values() { + for falsy in [ + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":0}}"#, + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":false}}"#, + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"0"}}"#, + r#"{"env":{"ANTHROPIC_BASE_URL":"https://provider.example"}}"#, + r#"{"alwaysThinkingEnabled":false}"#, + "not valid json", + ] { + assert!( + !common_config_snippet_disables_monitor_traffic(&AppType::Claude, falsy), + "expected no detection for snippet: {falsy}" + ); + } + } + + #[test] + fn disables_monitor_traffic_is_noop_for_non_claude_apps() { + let snippet = r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":1}}"#; + for app_type in [ + AppType::Gemini, + AppType::OpenCode, + AppType::Hermes, + AppType::OpenClaw, + AppType::Codex, + ] { + assert!( + !common_config_snippet_disables_monitor_traffic(&app_type, snippet), + "expected no detection for non-Claude app type: {app_type:?}" + ); + } + } + #[test] #[serial] fn set_stores_claude_snippet_without_enabling_provider_live_config() { @@ -465,6 +560,36 @@ mod tests { ); } + #[test] + #[serial] + fn set_succeeds_when_snippet_disables_monitor_traffic() { + let (_temp_home, _env) = seed_current_claude_provider(); + + set( + AppType::Claude, + Some(r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":1}}"#), + None, + false, + ) + .expect("set should still succeed for a snippet that disables Monitor traffic"); + + let state = AppState::try_new().expect("reload state"); + let stored = state + .config + .read() + .expect("read config") + .common_config_snippets + .claude + .clone() + .expect("stored claude snippet"); + let stored_json: serde_json::Value = + serde_json::from_str(&stored).expect("stored snippet should be valid JSON"); + assert_eq!( + stored_json["env"]["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"], + 1 + ); + } + #[test] #[serial] fn set_updates_live_config_for_common_config_enabled_claude_provider() { diff --git a/src-tauri/src/cli/commands/mod.rs b/src-tauri/src/cli/commands/mod.rs index b258b3fe..d9696331 100644 --- a/src-tauri/src/cli/commands/mod.rs +++ b/src-tauri/src/cli/commands/mod.rs @@ -2,7 +2,7 @@ pub(crate) mod app_targets; pub mod auth; pub mod completions; pub mod config; -mod config_common; +pub(crate) mod config_common; pub(crate) mod config_openclaw; pub mod config_s3; pub mod config_webdav; diff --git a/src-tauri/src/cli/i18n.rs b/src-tauri/src/cli/i18n.rs index 2c2d1d15..6c71f0a2 100644 --- a/src-tauri/src/cli/i18n.rs +++ b/src-tauri/src/cli/i18n.rs @@ -12743,6 +12743,14 @@ pub mod texts { } } + pub fn common_config_snippet_disables_monitor_traffic() -> &'static str { + if is_chinese() { + "警告:CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC 会禁用 Claude Code 的 Monitor 工具(及其他“非必要流量”)。" + } else { + "Warning: CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC disables Claude Code's Monitor tool (and other \"nonessential traffic\")." + } + } + pub fn common_config_snippet_extracted() -> &'static str { if is_chinese() { "已从当前编辑内容提取通用配置片段" diff --git a/src-tauri/src/cli/tui/runtime_actions/editor.rs b/src-tauri/src/cli/tui/runtime_actions/editor.rs index 117f0c4a..c2ce0c63 100644 --- a/src-tauri/src/cli/tui/runtime_actions/editor.rs +++ b/src-tauri/src/cli/tui/runtime_actions/editor.rs @@ -1259,7 +1259,26 @@ fn submit_config_common_snippet( } ctx.app.editor = None; - ctx.app.push_toast(toast, ToastKind::Success); + let disables_monitor_traffic = next_snippet.as_deref().is_some_and(|snippet| { + crate::cli::commands::config_common::common_config_snippet_disables_monitor_traffic( + &app_type, snippet, + ) + }); + if disables_monitor_traffic { + // `push_toast` replaces the app's single current toast (there is no + // toast queue), so the Monitor-traffic warning is combined into one + // message rather than pushed as a second toast that would silently + // overwrite the save confirmation. + ctx.app.push_toast( + format!( + "{toast} {}", + texts::common_config_snippet_disables_monitor_traffic() + ), + ToastKind::Warning, + ); + } else { + ctx.app.push_toast(toast, ToastKind::Success); + } *ctx.data = UiData::load(&ctx.app.app_type)?; if matches!(source, CommonSnippetViewSource::ProviderForm) { if let Some(next_form) = reconciled_codex_form { @@ -2094,6 +2113,130 @@ mod tests { assert!(!form.claude_tool_search); } + #[test] + #[serial(home_settings)] + fn submit_config_common_snippet_warns_when_disabling_monitor_traffic() { + let mut fixture = runtime_ctx(AppType::Claude); + let mut ctx = RuntimeActionContext { + terminal: &mut fixture.terminal, + app: &mut fixture.app, + data: &mut fixture.data, + speedtest_req_tx: None, + stream_check_req_tx: None, + skills_req_tx: None, + proxy_req_tx: None, + proxy_loading: &mut fixture.proxy_loading, + local_env_req_tx: None, + session_req_tx: None, + webdav_req_tx: None, + webdav_loading: &mut fixture.webdav_loading, + update_req_tx: None, + update_check: &mut fixture.update_check, + model_fetch_req_tx: None, + managed_auth_req_tx: None, + }; + + super::submit( + &mut ctx, + EditorSubmit::ConfigCommonSnippet { + app_type: AppType::Claude, + source: crate::cli::tui::app::CommonSnippetViewSource::Global, + }, + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":1}}"#.to_string(), + ) + .expect("common snippet submit should succeed even when it disables Monitor traffic"); + + let toast = ctx.app.toast.as_ref().expect("expected a toast"); + assert_eq!(toast.kind, ToastKind::Warning); + assert!( + toast + .message + .contains(texts::common_config_snippet_disables_monitor_traffic()), + "toast should surface the Monitor-traffic warning: {}", + toast.message + ); + } + + #[test] + #[serial(home_settings)] + fn submit_config_common_snippet_no_warning_when_key_absent() { + let mut fixture = runtime_ctx(AppType::Claude); + let mut ctx = RuntimeActionContext { + terminal: &mut fixture.terminal, + app: &mut fixture.app, + data: &mut fixture.data, + speedtest_req_tx: None, + stream_check_req_tx: None, + skills_req_tx: None, + proxy_req_tx: None, + proxy_loading: &mut fixture.proxy_loading, + local_env_req_tx: None, + session_req_tx: None, + webdav_req_tx: None, + webdav_loading: &mut fixture.webdav_loading, + update_req_tx: None, + update_check: &mut fixture.update_check, + model_fetch_req_tx: None, + managed_auth_req_tx: None, + }; + + super::submit( + &mut ctx, + EditorSubmit::ConfigCommonSnippet { + app_type: AppType::Claude, + source: crate::cli::tui::app::CommonSnippetViewSource::Global, + }, + r#"{"env":{"ENABLE_TOOL_SEARCH":"true"}}"#.to_string(), + ) + .expect("common snippet submit should succeed"); + + let toast = ctx.app.toast.as_ref().expect("expected a toast"); + assert_eq!(toast.kind, ToastKind::Success); + assert!(!toast + .message + .contains(texts::common_config_snippet_disables_monitor_traffic())); + } + + #[test] + #[serial(home_settings)] + fn submit_config_common_snippet_no_warning_for_non_claude_app() { + let mut fixture = runtime_ctx(AppType::Codex); + let mut ctx = RuntimeActionContext { + terminal: &mut fixture.terminal, + app: &mut fixture.app, + data: &mut fixture.data, + speedtest_req_tx: None, + stream_check_req_tx: None, + skills_req_tx: None, + proxy_req_tx: None, + proxy_loading: &mut fixture.proxy_loading, + local_env_req_tx: None, + session_req_tx: None, + webdav_req_tx: None, + webdav_loading: &mut fixture.webdav_loading, + update_req_tx: None, + update_check: &mut fixture.update_check, + model_fetch_req_tx: None, + managed_auth_req_tx: None, + }; + + super::submit( + &mut ctx, + EditorSubmit::ConfigCommonSnippet { + app_type: AppType::Codex, + source: crate::cli::tui::app::CommonSnippetViewSource::Global, + }, + "model_reasoning_effort = \"high\"".to_string(), + ) + .expect("common snippet submit should succeed for Codex"); + + let toast = ctx.app.toast.as_ref().expect("expected a toast"); + assert_eq!(toast.kind, ToastKind::Success); + assert!(!toast + .message + .contains(texts::common_config_snippet_disables_monitor_traffic())); + } + #[test] #[serial(home_settings)] fn submit_common_snippet_reconciles_malformed_old_codex_form_to_saved_replacement() { From eb3fbcbb48d8ae5235679c95ef331c4bb698c673 Mon Sep 17 00:00:00 2001 From: Som Date: Wed, 19 Aug 2026 19:38:48 +0000 Subject: [PATCH 2/4] docs: flag Monitor-tool side effect in common-config example The documented CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC snippet disables Claude Code's Monitor tool. Add an inline comment and a callout next to the example in both READMEs so copying it isn't a silent surprise. Related to #184 --- README.md | 3 +++ README_ZH.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index d9fc7f2d..cac1bc12 100644 --- a/README.md +++ b/README.md @@ -441,6 +441,7 @@ cc-switch config validate # Validate config file # Common snippet (shared settings across providers) # Tries to refresh live config when applicable (`--apply` is kept only as a compatibility flag) cc-switch --app claude config common show +# Warning: CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC disables Claude Code's Monitor tool (see note below) cc-switch --app claude config common set --snippet '{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":1},"includeCoAuthoredBy":false}' cc-switch --app claude config common clear @@ -469,6 +470,8 @@ cc-switch config webdav migrate-v1-to-v2 cc-switch config reset # Reset to default configuration ``` +> **Warning:** The `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` snippet above disables Claude Code's Monitor tool (and other "nonessential traffic"). CC Switch now warns when you set this, but it isn't a validation error — the snippet is still applied. Only set it if you don't need Monitor. + ### 🌉 Proxy Management & Model Relay Inspect and control daemon-managed per-app proxy routes for supported apps. diff --git a/README_ZH.md b/README_ZH.md index af359f2c..892e2fec 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -443,6 +443,7 @@ cc-switch config validate # 验证配置文件 # 通用配置片段(跨所有供应商共享设置) # 会在适用时尝试刷新 live config(`--apply` 仅保留为兼容参数) cc-switch --app claude config common show +# 警告:CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC 会禁用 Claude Code 的 Monitor 工具(见下方说明) cc-switch --app claude config common set --snippet '{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":1},"includeCoAuthoredBy":false}' cc-switch --app claude config common clear @@ -471,6 +472,8 @@ cc-switch config webdav migrate-v1-to-v2 cc-switch config reset # 重置为默认配置 ``` +> **警告:** 上面示例中的 `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` 片段会禁用 Claude Code 的 Monitor 工具(及其他“非必要流量”)。CC Switch 现在会在你设置该值时给出警告,但这不是校验错误——片段仍会被应用。仅在确实不需要 Monitor 时才设置它。 + ### 🌉 代理管理与模型接入 查看并控制由守护进程管理的按应用代理路由。 From 8fdae25a8c7871a7fae025f65f4e2885f8cec605 Mon Sep 17 00:00:00 2001 From: Som Date: Wed, 19 Aug 2026 19:51:48 +0000 Subject: [PATCH 3/4] refactor(config): dedupe monitor-traffic warning print in set/extract Both call sites repeated the same "print success, then conditionally print the warning" block. Extract print_monitor_traffic_warning_if_disabled to remove the duplication. --- src-tauri/src/cli/commands/config_common.rs | 23 ++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src-tauri/src/cli/commands/config_common.rs b/src-tauri/src/cli/commands/config_common.rs index cb680900..efa0b298 100644 --- a/src-tauri/src/cli/commands/config_common.rs +++ b/src-tauri/src/cli/commands/config_common.rs @@ -237,6 +237,15 @@ pub(crate) fn common_config_snippet_disables_monitor_traffic( } } +fn print_monitor_traffic_warning_if_disabled(disables_monitor_traffic: bool) { + if disables_monitor_traffic { + println!( + "{}", + warning(texts::common_config_snippet_disables_monitor_traffic()) + ); + } +} + fn format( app_type: AppType, snippet_text: Option<&str>, @@ -275,12 +284,7 @@ fn set( "{}", success(&texts::config_common_snippet_set_for_app(app_type.as_str())) ); - if disables_monitor_traffic { - println!( - "{}", - warning(texts::common_config_snippet_disables_monitor_traffic()) - ); - } + print_monitor_traffic_warning_if_disabled(disables_monitor_traffic); let current_id = if app_type.is_additive_mode() { String::new() @@ -342,12 +346,7 @@ fn extract( Some(snippet.clone()), )?; println!("{}", success(texts::common_config_snippet_extracted())); - if disables_monitor_traffic { - println!( - "{}", - warning(texts::common_config_snippet_disables_monitor_traffic()) - ); - } + print_monitor_traffic_warning_if_disabled(disables_monitor_traffic); if !snippet.trim().is_empty() { println!(); println!("{}", snippet); From 209a46cba33408f9ca5acbc81f1a1af5b87c6c1d Mon Sep 17 00:00:00 2001 From: Som Date: Wed, 19 Aug 2026 20:35:15 +0000 Subject: [PATCH 4/4] fix(config): move monitor-traffic detection to services, fix toast overwrite Code review findings: - AGENTS.md/CLAUDE.md require durable logic shared between the CLI and TUI to live in src/services/, not src/cli/commands/. Move common_config_snippet_disables_monitor_traffic onto ProviderService and have both the CLI and TUI call it from there. - The TUI save handler could still silently drop the Monitor-traffic warning: a later Codex-form-reconciliation-error toast could overwrite the just-pushed warning toast, since there is no toast queue. Defer the final toast decision until after reconciliation runs, and always fold the warning into whichever toast ends up being shown. --- src-tauri/src/cli/commands/config_common.rs | 83 +--------------- src-tauri/src/cli/commands/mod.rs | 2 +- .../src/cli/tui/runtime_actions/editor.rs | 95 +++++++++++++++---- src-tauri/src/services/provider/mod.rs | 33 +++++++ src-tauri/src/services/provider/tests.rs | 57 +++++++++++ 5 files changed, 169 insertions(+), 101 deletions(-) diff --git a/src-tauri/src/cli/commands/config_common.rs b/src-tauri/src/cli/commands/config_common.rs index efa0b298..0a869e6a 100644 --- a/src-tauri/src/cli/commands/config_common.rs +++ b/src-tauri/src/cli/commands/config_common.rs @@ -207,36 +207,6 @@ fn canonical_common_snippet(app_type: AppType, raw: &str) -> Result bool { - if *app_type != AppType::Claude { - return false; - } - - let Ok(value) = serde_json::from_str::(canonical_snippet) else { - return false; - }; - - let Some(raw) = value.get("env").and_then(|env| { - env.get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC") - }) else { - return false; - }; - - match raw { - serde_json::Value::Number(n) => n.as_i64() == Some(1), - serde_json::Value::Bool(b) => *b, - serde_json::Value::String(s) => s == "1" || s == "true", - _ => false, - } -} - fn print_monitor_traffic_warning_if_disabled(disables_monitor_traffic: bool) { if disables_monitor_traffic { println!( @@ -275,7 +245,7 @@ fn set( )?; let snippet = canonical_common_snippet(app_type.clone(), &raw)?.unwrap_or_default(); let disables_monitor_traffic = - common_config_snippet_disables_monitor_traffic(&app_type, &snippet); + ProviderService::common_config_snippet_disables_monitor_traffic(&app_type, &snippet); let state = get_state()?; ProviderService::set_common_config_snippet(&state, app_type.clone(), Some(snippet))?; @@ -339,7 +309,7 @@ fn extract( if save { let snippet = canonical_common_snippet(app_type.clone(), &extracted)?.unwrap_or_default(); let disables_monitor_traffic = - common_config_snippet_disables_monitor_traffic(&app_type, &snippet); + ProviderService::common_config_snippet_disables_monitor_traffic(&app_type, &snippet); ProviderService::set_common_config_snippet( &state, app_type.clone(), @@ -476,55 +446,6 @@ mod tests { seed_current_codex_provider_with_meta(None) } - #[test] - fn disables_monitor_traffic_detects_truthy_values() { - for truthy in [ - r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":1}}"#, - r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":true}}"#, - r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1"}}"#, - r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"true"}}"#, - ] { - assert!( - common_config_snippet_disables_monitor_traffic(&AppType::Claude, truthy), - "expected truthy detection for snippet: {truthy}" - ); - } - } - - #[test] - fn disables_monitor_traffic_ignores_falsy_or_absent_values() { - for falsy in [ - r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":0}}"#, - r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":false}}"#, - r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"0"}}"#, - r#"{"env":{"ANTHROPIC_BASE_URL":"https://provider.example"}}"#, - r#"{"alwaysThinkingEnabled":false}"#, - "not valid json", - ] { - assert!( - !common_config_snippet_disables_monitor_traffic(&AppType::Claude, falsy), - "expected no detection for snippet: {falsy}" - ); - } - } - - #[test] - fn disables_monitor_traffic_is_noop_for_non_claude_apps() { - let snippet = r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":1}}"#; - for app_type in [ - AppType::Gemini, - AppType::OpenCode, - AppType::Hermes, - AppType::OpenClaw, - AppType::Codex, - ] { - assert!( - !common_config_snippet_disables_monitor_traffic(&app_type, snippet), - "expected no detection for non-Claude app type: {app_type:?}" - ); - } - } - #[test] #[serial] fn set_stores_claude_snippet_without_enabling_provider_live_config() { diff --git a/src-tauri/src/cli/commands/mod.rs b/src-tauri/src/cli/commands/mod.rs index d9696331..b258b3fe 100644 --- a/src-tauri/src/cli/commands/mod.rs +++ b/src-tauri/src/cli/commands/mod.rs @@ -2,7 +2,7 @@ pub(crate) mod app_targets; pub mod auth; pub mod completions; pub mod config; -pub(crate) mod config_common; +mod config_common; pub(crate) mod config_openclaw; pub mod config_s3; pub mod config_webdav; diff --git a/src-tauri/src/cli/tui/runtime_actions/editor.rs b/src-tauri/src/cli/tui/runtime_actions/editor.rs index c2ce0c63..e7bb4e4f 100644 --- a/src-tauri/src/cli/tui/runtime_actions/editor.rs +++ b/src-tauri/src/cli/tui/runtime_actions/editor.rs @@ -1260,25 +1260,14 @@ fn submit_config_common_snippet( ctx.app.editor = None; let disables_monitor_traffic = next_snippet.as_deref().is_some_and(|snippet| { - crate::cli::commands::config_common::common_config_snippet_disables_monitor_traffic( - &app_type, snippet, - ) + ProviderService::common_config_snippet_disables_monitor_traffic(&app_type, snippet) }); - if disables_monitor_traffic { - // `push_toast` replaces the app's single current toast (there is no - // toast queue), so the Monitor-traffic warning is combined into one - // message rather than pushed as a second toast that would silently - // overwrite the save confirmation. - ctx.app.push_toast( - format!( - "{toast} {}", - texts::common_config_snippet_disables_monitor_traffic() - ), - ToastKind::Warning, - ); - } else { - ctx.app.push_toast(toast, ToastKind::Success); - } + // Decide the final toast text/kind after reconciliation below runs, then + // push exactly once. `push_toast` replaces the app's single current toast + // (there is no toast queue), so pushing here first and potentially again + // in the reconciliation branch would let a later reconciliation-error + // toast silently swallow the Monitor-traffic warning. + let mut final_toast = (toast.to_string(), ToastKind::Success); *ctx.data = UiData::load(&ctx.app.app_type)?; if matches!(source, CommonSnippetViewSource::ProviderForm) { if let Some(next_form) = reconciled_codex_form { @@ -1298,10 +1287,21 @@ fn submit_config_common_snippet( _ => Ok(()), }; if let Err(err) = refresh_result { - ctx.app.push_toast(err, ToastKind::Warning); + final_toast = (err, ToastKind::Warning); } } } + if disables_monitor_traffic { + final_toast = ( + format!( + "{} {}", + final_toast.0, + texts::common_config_snippet_disables_monitor_traffic() + ), + ToastKind::Warning, + ); + } + ctx.app.push_toast(final_toast.0, final_toast.1); if matches!(source, CommonSnippetViewSource::Global) { ctx.app.overlay = crate::cli::tui::app::Overlay::None; } @@ -2157,6 +2157,63 @@ mod tests { ); } + #[test] + #[serial(home_settings)] + fn submit_config_common_snippet_warns_when_disabling_monitor_traffic_via_provider_form() { + // Regression coverage for the ProviderForm reconciliation path + // (`replace_common_config_snippet`), which pushes its own toast on + // failure and previously could silently overwrite the Monitor + // warning. This exercises the successful-reconciliation case; the + // final-toast merge (see the fix at the `submit_config_common_snippet` + // call site) ensures the warning survives regardless of which branch + // sets `final_toast` first. + let mut fixture = runtime_ctx(AppType::Claude); + fixture.app.form = Some(FormState::ProviderAdd( + crate::cli::tui::form::ProviderAddFormState::new_with_common_snippet( + AppType::Claude, + r#"{"env":{"ENABLE_TOOL_SEARCH":"true"}}"#, + ), + )); + let mut ctx = RuntimeActionContext { + terminal: &mut fixture.terminal, + app: &mut fixture.app, + data: &mut fixture.data, + speedtest_req_tx: None, + stream_check_req_tx: None, + skills_req_tx: None, + proxy_req_tx: None, + proxy_loading: &mut fixture.proxy_loading, + local_env_req_tx: None, + session_req_tx: None, + webdav_req_tx: None, + webdav_loading: &mut fixture.webdav_loading, + update_req_tx: None, + update_check: &mut fixture.update_check, + model_fetch_req_tx: None, + managed_auth_req_tx: None, + }; + + super::submit( + &mut ctx, + EditorSubmit::ConfigCommonSnippet { + app_type: AppType::Claude, + source: crate::cli::tui::app::CommonSnippetViewSource::ProviderForm, + }, + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":1}}"#.to_string(), + ) + .expect("common snippet submit should succeed via the ProviderForm source too"); + + let toast = ctx.app.toast.as_ref().expect("expected a toast"); + assert_eq!(toast.kind, ToastKind::Warning); + assert!( + toast + .message + .contains(texts::common_config_snippet_disables_monitor_traffic()), + "warning must survive the ProviderForm reconciliation branch: {}", + toast.message + ); + } + #[test] #[serial(home_settings)] fn submit_config_common_snippet_no_warning_when_key_absent() { diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index 64299901..ff7268a9 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -1723,6 +1723,39 @@ impl ProviderService { Ok(()) } + /// Detects whether a canonicalized Claude common-config snippet sets + /// `env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` to a truthy value, which + /// silently disables Claude Code's Monitor tool. No-op for any app type + /// other than Claude, since the env var only affects Claude Code. Shared + /// by the CLI and TUI common-config save paths, so it lives here rather + /// than in `cli::commands` (see AGENTS.md "CLI architecture"). + pub fn common_config_snippet_disables_monitor_traffic( + app_type: &AppType, + canonical_snippet: &str, + ) -> bool { + if *app_type != AppType::Claude { + return false; + } + + let Ok(value) = serde_json::from_str::(canonical_snippet) else { + return false; + }; + + let Some(raw) = value + .get("env") + .and_then(|env| env.get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")) + else { + return false; + }; + + match raw { + Value::Number(n) => n.as_i64() == Some(1), + Value::Bool(b) => *b, + Value::String(s) => s == "1" || s == "true", + _ => false, + } + } + pub fn set_common_config_snippet( state: &AppState, app_type: AppType, diff --git a/src-tauri/src/services/provider/tests.rs b/src-tauri/src/services/provider/tests.rs index a02af12d..156576de 100644 --- a/src-tauri/src/services/provider/tests.rs +++ b/src-tauri/src/services/provider/tests.rs @@ -7317,3 +7317,60 @@ fn delete_rejects_last_failover_queue_provider_while_active() { .expect("read queued provider") .is_some()); } + +#[test] +fn disables_monitor_traffic_detects_truthy_values() { + for truthy in [ + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":1}}"#, + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":true}}"#, + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1"}}"#, + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"true"}}"#, + ] { + assert!( + ProviderService::common_config_snippet_disables_monitor_traffic( + &AppType::Claude, + truthy + ), + "expected truthy detection for snippet: {truthy}" + ); + } +} + +#[test] +fn disables_monitor_traffic_ignores_falsy_or_absent_values() { + for falsy in [ + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":0}}"#, + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":false}}"#, + r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"0"}}"#, + r#"{"env":{"ANTHROPIC_BASE_URL":"https://provider.example"}}"#, + r#"{"alwaysThinkingEnabled":false}"#, + "not valid json", + ] { + assert!( + !ProviderService::common_config_snippet_disables_monitor_traffic( + &AppType::Claude, + falsy + ), + "expected no detection for snippet: {falsy}" + ); + } +} + +#[test] +fn disables_monitor_traffic_is_noop_for_non_claude_apps() { + let snippet = r#"{"env":{"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":1}}"#; + for app_type in [ + AppType::Gemini, + AppType::OpenCode, + AppType::Hermes, + AppType::OpenClaw, + AppType::Codex, + ] { + assert!( + !ProviderService::common_config_snippet_disables_monitor_traffic( + &app_type, snippet + ), + "expected no detection for non-Claude app type: {app_type:?}" + ); + } +}