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 时才设置它。 + ### 🌉 代理管理与模型接入 查看并控制由守护进程管理的按应用代理路由。 diff --git a/src-tauri/src/cli/commands/config_common.rs b/src-tauri/src/cli/commands/config_common.rs index b06e93a8..0a869e6a 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,15 @@ fn canonical_common_snippet(app_type: AppType, raw: &str) -> Result, @@ -235,6 +244,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 = + 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))?; @@ -243,6 +254,7 @@ fn set( "{}", success(&texts::config_common_snippet_set_for_app(app_type.as_str())) ); + print_monitor_traffic_warning_if_disabled(disables_monitor_traffic); let current_id = if app_type.is_additive_mode() { String::new() @@ -296,12 +308,15 @@ fn extract( if save { let snippet = canonical_common_snippet(app_type.clone(), &extracted)?.unwrap_or_default(); + let disables_monitor_traffic = + ProviderService::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())); + print_monitor_traffic_warning_if_disabled(disables_monitor_traffic); if !snippet.trim().is_empty() { println!(); println!("{}", snippet); @@ -465,6 +480,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/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..e7bb4e4f 100644 --- a/src-tauri/src/cli/tui/runtime_actions/editor.rs +++ b/src-tauri/src/cli/tui/runtime_actions/editor.rs @@ -1259,7 +1259,15 @@ 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| { + ProviderService::common_config_snippet_disables_monitor_traffic(&app_type, snippet) + }); + // 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 { @@ -1279,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; } @@ -2094,6 +2113,187 @@ 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_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() { + 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() { 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:?}" + ); + } +}