From b68f02e2c8f3f4ed58053615485bd3ec6f1a701d Mon Sep 17 00:00:00 2001 From: paigeman <53284808+paigeman@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:13:24 +0800 Subject: [PATCH] fix(tui): align proxy listen port editing with active workers - allow editing the selected app proxy listen port while the proxy is running for other apps - keep listen address blocked while the local proxy is running - add active-worker helper for proxy snapshots and reuse it across TUI views and handlers - split address and listen-port stop messages for hints, toasts, and runtime fallback - cover running-proxy editability, active-worker blocking, and submit-time blocking in TUI tests --- src-tauri/src/cli/i18n.rs | 28 ++++- .../src/cli/i18n/texts/config_actions.rs | 28 ++++- src-tauri/src/cli/tui/app/content_config.rs | 16 +-- .../cli/tui/app/overlay_handlers/dialogs.rs | 6 +- src-tauri/src/cli/tui/app/tests.rs | 105 +++++++++++++++++- src-tauri/src/cli/tui/data.rs | 10 +- .../src/cli/tui/runtime_actions/settings.rs | 4 +- src-tauri/src/cli/tui/ui/config.rs | 23 ++-- src-tauri/src/cli/tui/ui/tests.rs | 34 +++++- 9 files changed, 213 insertions(+), 41 deletions(-) diff --git a/src-tauri/src/cli/i18n.rs b/src-tauri/src/cli/i18n.rs index 7c81804f..1fddc786 100644 --- a/src-tauri/src/cli/i18n.rs +++ b/src-tauri/src/cli/i18n.rs @@ -4260,11 +4260,19 @@ pub mod texts { } } - pub fn tui_settings_proxy_stop_before_edit_hint() -> &'static str { + pub fn tui_settings_proxy_stop_before_edit_hint(current_app_is_active: bool) -> &'static str { if is_chinese() { - "请先停止本地代理,再修改监听地址或端口" + if current_app_is_active { + "修改监听地址:需先停止本地代理。修改监听端口:需先停止当前应用的代理路由。改完后重新启动路由生效。" + } else { + "修改监听地址:需先停止本地代理。监听端口可以修改。改完后重新启动路由生效。" + } } else { - "Stop the local proxy before editing listen address or port" + if current_app_is_active { + "Listen address: stop the proxy to edit. Listen port: stop this app's route to edit. Restart routing after changes." + } else { + "Listen address: stop the proxy to edit. Listen port can be edited. Restart routing after changes." + } } } @@ -4300,11 +4308,19 @@ pub mod texts { } } - pub fn tui_toast_proxy_settings_stop_before_edit() -> &'static str { + pub fn tui_toast_proxy_settings_stop_proxy_before_edit_address() -> &'static str { + if is_chinese() { + "本地代理正在运行。请先停止代理,再修改监听地址。" + } else { + "The local proxy is running. Stop it before editing listen address." + } + } + + pub fn tui_toast_proxy_settings_stop_app_route_before_edit_port() -> &'static str { if is_chinese() { - "本地代理正在运行。请先停止代理,再修改监听地址或端口。" + "当前应用正在使用代理。请先停止当前应用的代理路由,再修改监听端口。" } else { - "The local proxy is running. Stop it before editing listen address or port." + "This app is using the proxy. Stop this app's proxy route before editing listen port." } } diff --git a/src-tauri/src/cli/i18n/texts/config_actions.rs b/src-tauri/src/cli/i18n/texts/config_actions.rs index 28c165d1..32f5279e 100644 --- a/src-tauri/src/cli/i18n/texts/config_actions.rs +++ b/src-tauri/src/cli/i18n/texts/config_actions.rs @@ -7,11 +7,19 @@ pub fn tui_settings_proxy_restart_hint() -> &'static str { } } -pub fn tui_settings_proxy_stop_before_edit_hint() -> &'static str { +pub fn tui_settings_proxy_stop_before_edit_hint(current_app_has_active_worker: bool) -> &'static str { if is_chinese() { - "请先停止本地代理,再修改监听地址或端口" + if current_app_has_active_worker { + "修改监听地址:需先停止本地代理。修改监听端口:需先停止当前应用的代理路由。改完后重新启动路由生效。" + } else { + "修改监听地址:需先停止本地代理。监听端口可以修改。改完后重新启动路由生效。" + } } else { - "Stop the local proxy before editing listen address or port" + if current_app_has_active_worker { + "Listen address: stop the proxy to edit. Listen port: stop this app's route to edit. Restart routing after changes." + } else { + "Listen address: stop the proxy to edit. Listen port can be edited. Restart routing after changes." + } } } @@ -47,11 +55,19 @@ pub fn tui_toast_proxy_settings_restart_required() -> &'static str { } } -pub fn tui_toast_proxy_settings_stop_before_edit() -> &'static str { +pub fn tui_toast_proxy_settings_stop_proxy_before_edit_address() -> &'static str { + if is_chinese() { + "本地代理正在运行。请先停止代理,再修改监听地址。" + } else { + "The local proxy is running. Stop it before editing listen address." + } +} + +pub fn tui_toast_proxy_settings_stop_app_route_before_edit_port() -> &'static str { if is_chinese() { - "本地代理正在运行。请先停止代理,再修改监听地址或端口。" + "当前应用正在使用代理。请先停止当前应用的代理路由,再修改监听端口。" } else { - "The local proxy is running. Stop it before editing listen address or port." + "This app is using the proxy. Stop this app's proxy route before editing listen port." } } diff --git a/src-tauri/src/cli/tui/app/content_config.rs b/src-tauri/src/cli/tui/app/content_config.rs index b44c87bc..8371c57d 100644 --- a/src-tauri/src/cli/tui/app/content_config.rs +++ b/src-tauri/src/cli/tui/app/content_config.rs @@ -876,7 +876,7 @@ impl App { Some(LocalProxySettingsItem::ListenAddress) => { if data.proxy.running { self.push_toast( - texts::tui_toast_proxy_settings_stop_before_edit(), + texts::tui_toast_proxy_settings_stop_proxy_before_edit_address(), ToastKind::Info, ); return Action::None; @@ -891,9 +891,9 @@ impl App { Action::None } Some(LocalProxySettingsItem::ListenPort) => { - if data.proxy.running { + if data.proxy.has_active_worker_for(&self.app_type) { self.push_toast( - texts::tui_toast_proxy_settings_stop_before_edit(), + texts::tui_toast_proxy_settings_stop_app_route_before_edit_port(), ToastKind::Info, ); return Action::None; @@ -1110,6 +1110,10 @@ impl App { ), ]); } else { + let current_app_has_active_worker = data.proxy.has_active_worker_for(&self.app_type); + let port_edit_hint = + texts::tui_settings_proxy_stop_before_edit_hint(current_app_has_active_worker) + .to_string(); lines.extend([ format!( "{}: {}:{}", @@ -1117,11 +1121,7 @@ impl App { data.proxy.configured_listen_address, data.proxy.configured_listen_port ), - crate::t!( - "Stop the local proxy before editing listen address or port. Restart routing after those settings change.", - "修改监听地址或端口前需要先停止本地代理;改完后重新启动路由才会生效。" - ) - .to_string(), + port_edit_hint, ]); } diff --git a/src-tauri/src/cli/tui/app/overlay_handlers/dialogs.rs b/src-tauri/src/cli/tui/app/overlay_handlers/dialogs.rs index c09003f1..08704cc3 100644 --- a/src-tauri/src/cli/tui/app/overlay_handlers/dialogs.rs +++ b/src-tauri/src/cli/tui/app/overlay_handlers/dialogs.rs @@ -483,7 +483,7 @@ impl App { ) -> Action { if data.proxy.running { self.push_toast( - texts::tui_toast_proxy_settings_stop_before_edit(), + texts::tui_toast_proxy_settings_stop_proxy_before_edit_address(), ToastKind::Info, ); return Action::None; @@ -509,9 +509,9 @@ impl App { } fn handle_settings_proxy_listen_port_submit(&mut self, data: &UiData, raw: String) -> Action { - if data.proxy.running { + if data.proxy.has_active_worker_for(&self.app_type) { self.push_toast( - texts::tui_toast_proxy_settings_stop_before_edit(), + texts::tui_toast_proxy_settings_stop_app_route_before_edit_port(), ToastKind::Info, ); return Action::None; diff --git a/src-tauri/src/cli/tui/app/tests.rs b/src-tauri/src/cli/tui/app/tests.rs index ca8b701c..b5d99c63 100644 --- a/src-tauri/src/cli/tui/app/tests.rs +++ b/src-tauri/src/cli/tui/app/tests.rs @@ -9868,6 +9868,7 @@ mod tests { let mut data = UiData::default(); data.proxy.running = true; + data.proxy.claude_takeover = true; data.proxy.configured_listen_address = "127.0.0.1".to_string(); data.proxy.configured_listen_port = 15721; @@ -9880,7 +9881,106 @@ mod tests { message, kind: ToastKind::Info, .. - }) if message == "The local proxy is running. Stop it before editing listen address or port." + }) if message == "The local proxy is running. Stop it before editing listen address." + )); + } + + #[test] + fn settings_proxy_port_editable_when_proxy_running_but_app_not_routed() { + let mut app = App::new(Some(AppType::Claude)); + app.route = Route::SettingsProxy; + app.focus = Focus::Content; + app.settings_proxy_idx = LocalProxySettingsItem::ALL + .iter() + .position(|item| matches!(item, LocalProxySettingsItem::ListenPort)) + .expect("ListenPort missing"); + + let mut data = UiData::default(); + data.proxy.running = true; + data.proxy.claude_takeover = false; + data.proxy.configured_listen_port = 15721; + + let action = app.on_key(key(KeyCode::Enter), &data); + assert!(matches!(action, Action::None)); + assert!(matches!( + app.overlay, + Overlay::TextInput(TextInputState { + submit: TextSubmit::SettingsProxyListenPort, + .. + }) + )); + + app.overlay = Overlay::TextInput(TextInputState { + title: "Listen Port".to_string(), + prompt: "port".to_string(), + input: TextInput::new("15721".to_string()), + submit: TextSubmit::SettingsProxyListenPort, + secret: false, + }); + let action = app.on_key(key(KeyCode::Enter), &data); + assert!(matches!( + action, + Action::SetProxyListenPort { port } if port == 15721 + )); + } + + #[test] + fn settings_proxy_port_blocked_when_active_worker_exists() { + let mut app = App::new(Some(AppType::Claude)); + app.route = Route::SettingsProxy; + app.focus = Focus::Content; + app.settings_proxy_idx = LocalProxySettingsItem::ALL + .iter() + .position(|item| matches!(item, LocalProxySettingsItem::ListenPort)) + .expect("ListenPort missing"); + + let mut data = UiData::default(); + data.proxy.running = true; + data.proxy.active_worker_apps = + std::collections::HashSet::from([AppType::Claude.as_str().to_string()]); + data.proxy.configured_listen_port = 15721; + + let action = app.on_key(key(KeyCode::Enter), &data); + assert!(matches!(action, Action::None)); + assert!(matches!(app.overlay, Overlay::None)); + assert!(matches!( + app.toast.as_ref(), + Some(Toast { + message, + kind: ToastKind::Info, + .. + }) if message == "This app is using the proxy. Stop this app's proxy route before editing listen port." + )); + } + + #[test] + fn settings_proxy_port_submit_blocked_when_active_worker_starts_before_confirm() { + let mut app = App::new(Some(AppType::Claude)); + app.route = Route::SettingsProxy; + app.focus = Focus::Content; + app.overlay = Overlay::TextInput(TextInputState { + title: "Listen Port".to_string(), + prompt: "port".to_string(), + input: TextInput::new("15721".to_string()), + submit: TextSubmit::SettingsProxyListenPort, + secret: false, + }); + + let mut data = UiData::default(); + data.proxy.running = true; + data.proxy.active_worker_apps = + std::collections::HashSet::from([AppType::Claude.as_str().to_string()]); + + let action = app.on_key(key(KeyCode::Enter), &data); + assert!(matches!(action, Action::None)); + assert!(matches!(app.overlay, Overlay::None)); + assert!(matches!( + app.toast.as_ref(), + Some(Toast { + message, + kind: ToastKind::Info, + .. + }) if message == "This app is using the proxy. Stop this app's proxy route before editing listen port." )); } @@ -9975,6 +10075,7 @@ mod tests { let mut data = UiData::default(); data.proxy.running = true; + data.proxy.claude_takeover = true; let action = app.on_key(key(KeyCode::Enter), &data); assert!(matches!(action, Action::None)); @@ -9985,7 +10086,7 @@ mod tests { message, kind: ToastKind::Info, .. - }) if message == "The local proxy is running. Stop it before editing listen address or port." + }) if message == "The local proxy is running. Stop it before editing listen address." )); } diff --git a/src-tauri/src/cli/tui/data.rs b/src-tauri/src/cli/tui/data.rs index 772b4552..60e5eda4 100644 --- a/src-tauri/src/cli/tui/data.rs +++ b/src-tauri/src/cli/tui/data.rs @@ -299,6 +299,11 @@ pub struct ProxySnapshot { } impl ProxySnapshot { + pub fn has_active_worker_for(&self, app_type: &AppType) -> bool { + self.active_worker_apps + .contains(&app_type.as_str().to_ascii_lowercase()) + } + pub fn takeover_enabled_for(&self, app_type: &AppType) -> Option { match app_type { AppType::Claude => Some(self.claude_takeover), @@ -317,10 +322,7 @@ impl ProxySnapshot { } if self.managed_runtime && !self.active_worker_apps.is_empty() { - return Some( - self.active_worker_apps - .contains(&app_type.as_str().to_ascii_lowercase()), - ); + return Some(self.has_active_worker_for(app_type)); } Some(true) diff --git a/src-tauri/src/cli/tui/runtime_actions/settings.rs b/src-tauri/src/cli/tui/runtime_actions/settings.rs index 2d1bce6b..3b39d002 100644 --- a/src-tauri/src/cli/tui/runtime_actions/settings.rs +++ b/src-tauri/src/cli/tui/runtime_actions/settings.rs @@ -72,7 +72,7 @@ pub(super) fn set_proxy_listen_port( if app_running { *ctx.data = UiData::load(&ctx.app.app_type)?; ctx.app.push_toast( - texts::tui_toast_proxy_settings_stop_before_edit(), + texts::tui_toast_proxy_settings_stop_app_route_before_edit_port(), super::super::app::ToastKind::Info, ); return Ok(()); @@ -527,7 +527,7 @@ fn update_proxy_config( if status.running { *ctx.data = UiData::load(&ctx.app.app_type)?; ctx.app.push_toast( - texts::tui_toast_proxy_settings_stop_before_edit(), + texts::tui_toast_proxy_settings_stop_proxy_before_edit_address(), super::super::app::ToastKind::Info, ); return Ok(()); diff --git a/src-tauri/src/cli/tui/ui/config.rs b/src-tauri/src/cli/tui/ui/config.rs index 56efcf5b..85b2bd89 100644 --- a/src-tauri/src/cli/tui/ui/config.rs +++ b/src-tauri/src/cli/tui/ui/config.rs @@ -3163,7 +3163,12 @@ pub(super) fn render_settings_proxy( if app.focus == Focus::Content { let key_label = match LocalProxySettingsItem::ALL.get(app.settings_proxy_idx) { Some(LocalProxySettingsItem::AutoFailover) => texts::tui_key_toggle(), - _ if data.proxy.running => "", + Some(LocalProxySettingsItem::ListenAddress) if data.proxy.running => "", + Some(LocalProxySettingsItem::ListenPort) + if data.proxy.has_active_worker_for(&app.app_type) => + { + "" + } _ => texts::tui_key_edit(), }; if !key_label.is_empty() { @@ -3184,14 +3189,16 @@ pub(super) fn render_settings_proxy( state.select(Some(app.settings_proxy_idx)); frame.render_stateful_widget(table, inset_left(chunks[1], CONTENT_INSET_LEFT), &mut state); + let hint = if !data.proxy.running { + texts::tui_settings_proxy_restart_hint() + } else { + let current_app_has_active_worker = data.proxy.has_active_worker_for(&app.app_type); + texts::tui_settings_proxy_stop_before_edit_hint(current_app_has_active_worker) + }; frame.render_widget( - Paragraph::new(if data.proxy.running { - texts::tui_settings_proxy_stop_before_edit_hint() - } else { - texts::tui_settings_proxy_restart_hint() - }) - .alignment(Alignment::Center) - .style(Style::default().fg(theme.dim)), + Paragraph::new(hint) + .alignment(Alignment::Center) + .style(Style::default().fg(theme.dim)), chunks[2], ); } diff --git a/src-tauri/src/cli/tui/ui/tests.rs b/src-tauri/src/cli/tui/ui/tests.rs index 1fb1ca6c..9f01c85f 100644 --- a/src-tauri/src/cli/tui/ui/tests.rs +++ b/src-tauri/src/cli/tui/ui/tests.rs @@ -1255,14 +1255,44 @@ fn settings_proxy_route_hides_edit_key_when_proxy_is_running() { let mut data = minimal_data(&app.app_type); data.proxy.running = true; + data.proxy.active_worker_apps = + std::collections::HashSet::from([AppType::Claude.as_str().to_string()]); + data.proxy.configured_listen_address = "127.0.0.1".to_string(); + data.proxy.configured_listen_port = 15722; + + let buf = render(&app, &data); + let all = all_text(&buf); + + assert!(!all.contains("Enter edit")); + assert!(all.contains("Listen address: stop the proxy to edit")); + assert!(all.contains("Listen port: stop this app's route to edit")); +} + +#[test] +fn settings_proxy_shows_edit_key_when_running_but_app_not_routed() { + let _lock = lock_env(); + let _no_color = EnvGuard::remove("NO_COLOR"); + + let mut app = App::new(Some(AppType::Claude)); + app.route = Route::SettingsProxy; + app.focus = Focus::Content; + app.settings_proxy_idx = app::LocalProxySettingsItem::ALL + .iter() + .position(|item| matches!(item, app::LocalProxySettingsItem::ListenPort)) + .expect("ListenPort missing"); + + let mut data = minimal_data(&app.app_type); + data.proxy.running = true; + data.proxy.claude_takeover = false; data.proxy.configured_listen_address = "127.0.0.1".to_string(); data.proxy.configured_listen_port = 15722; let buf = render(&app, &data); let all = all_text(&buf); - assert!(!all.contains("Enter Edit")); - assert!(all.contains("Stop the local proxy before editing listen address or port")); + assert!(all.contains("Enter edit")); + assert!(all.contains("Listen port can be edited")); + assert!(!all.contains("Listen port: stop this app's route to edit")); } static ENV_LOCK: Mutex<()> = Mutex::new(());