diff --git a/crates/op-editor-core/src/agent_settings_button_state.rs b/crates/op-editor-core/src/agent_settings_button_state.rs index 374f672d2..508f9b979 100644 --- a/crates/op-editor-core/src/agent_settings_button_state.rs +++ b/crates/op-editor-core/src/agent_settings_button_state.rs @@ -13,6 +13,7 @@ pub enum AgentSettingsButton { BuiltinCancelDraft, AcpEdit(usize), AcpRemove(usize), + AcpConnection(usize), AcpSaveDraft, AcpCancelDraft, McpServer, diff --git a/crates/op-editor-ui/src/widgets/agent_settings_acp.rs b/crates/op-editor-ui/src/widgets/agent_settings_acp.rs index 686841247..1b84b71e6 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_acp.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_acp.rs @@ -9,10 +9,11 @@ use crate::widgets::agent_settings_header_action::{ header_action_rect, header_action_text_baseline_y, header_action_text_x, }; use crate::widgets::agent_settings_i18n::t as t_settings; -use crate::widgets::button::paint_ghost_button_feedback; +use crate::widgets::button::{paint_ghost_button_feedback, tokens_from_theme}; use crate::widgets::icons::{draw_icon, Icon}; use crate::widgets::PaintCx; use crate::{Color, Point2D, Rect, TextLayout}; +use jian_widgets::components::button::{Button, ButtonVariant}; use op_editor_core::agent_settings::{ AcpAgentConfig, AcpAgentField, AcpConnectionType, AgentSettings, SettingsFocus, }; @@ -359,7 +360,7 @@ fn paint_compact_acp_card( )), ); } - paint_connection_button(cx, theme, ui, agent, card); + paint_connection_button(cx, theme, ui, agent, index, card); } #[allow(clippy::too_many_arguments)] @@ -421,17 +422,35 @@ fn paint_connection_button( theme: &Theme, ui: &EditorUiState, agent: &AcpAgentConfig, + index: usize, card: Rect, ) { let btn = connection_button_rect(card); let enabled = agent.ready() || agent.connected; - let bg = if agent.connected { - theme.muted - } else if enabled { - theme.primary + if agent.connected { + cx.backend.fill_round_rect(btn, 6.0, theme.muted); + cx.backend.stroke_round_rect(btn, 6.0, theme.border, 1.0); } else { - theme.button_hover - }; + Button { + label: "", + icon_d: None, + variant: if enabled { + ButtonVariant::Primary + } else { + ButtonVariant::Outline + }, + enabled: true, + hovered: !enabled, + pressed: false, + font_size: 12.0, + } + .paint(cx.backend, btn, &tokens_from_theme(theme)); + } + if ui.button_pressed(ButtonPressTarget::AgentSettings( + AgentSettingsButton::AcpConnection(index), + )) { + paint_ghost_button_feedback(cx.backend, theme, btn, false, true); + } let fg = if agent.connected { Color { r: 0.93, @@ -444,8 +463,6 @@ fn paint_connection_button( } else { theme.muted_foreground }; - cx.backend.fill_round_rect(btn, 6.0, bg); - cx.backend.stroke_round_rect(btn, 6.0, theme.border, 1.0); let label = if agent.connected { t_settings(ui, "settings.agents.disconnect") } else if agent.ready() { diff --git a/crates/op-editor-ui/src/widgets/agent_settings_compact_action_tests.rs b/crates/op-editor-ui/src/widgets/agent_settings_compact_action_tests.rs index 0d0d004c6..5b9f926b3 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_compact_action_tests.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_compact_action_tests.rs @@ -119,6 +119,17 @@ fn acp_remove_rect(panel: Rect) -> Rect { } } +fn acp_connection_rect(panel: Rect) -> Rect { + let card = acp_card_rect_after_empty_builtin(panel); + Rect { + origin: Point2D::new( + card.origin.x + card.size.x - 12.0 - CONNECT_BTN_W, + card.origin.y + (card.size.y - 28.0) / 2.0, + ), + size: Point2D::new(CONNECT_BTN_W, 28.0), + } +} + #[test] fn pressed_builtin_compact_actions_use_shared_button_feedback() { for (button, expected_rect) in [ @@ -205,3 +216,36 @@ fn pressed_acp_compact_actions_use_shared_button_feedback() { ); } } + +#[test] +fn pressed_acp_connection_button_uses_shared_button_feedback() { + let mut state = EditorState::default(); + state.editor_ui.theme_mode = ThemeMode::Light; + state.editor_ui.agent_settings.tab = AgentSettingsTab::Agents; + state.editor_ui.agent_settings.add_acp_agent(); + state.editor_ui.agent_settings.acp_agents[0].command = "op-agent".into(); + state.editor_ui.pressed_button = Some(ButtonPressTarget::AgentSettings( + AgentSettingsButton::AcpConnection(0), + )); + let panel = AgentSettingsPanel::for_editor(&state); + let rect = panel.rect(1200.0, 800.0); + let target = acp_connection_rect(rect); + let expected = panel + .theme + .button_hover + .with_alpha(panel.theme.button_hover.a * 1.8); + let mut backend = CaptureBackend::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + + panel.paint(&mut cx, rect); + + assert!( + backend + .round_fills + .iter() + .any(|(r, color)| rect_eq(*r, target) && color_eq(*color, expected)), + "pressed ACP connection button should paint the shared pressed feedback token" + ); +} diff --git a/crates/op-editor-ui/src/widgets/editor_state_ext.rs b/crates/op-editor-ui/src/widgets/editor_state_ext.rs index 5c6b3afe5..5efa605b6 100644 --- a/crates/op-editor-ui/src/widgets/editor_state_ext.rs +++ b/crates/op-editor-ui/src/widgets/editor_state_ext.rs @@ -110,6 +110,7 @@ pub fn agent_settings_button( W::CancelBuiltinAgentDraft => Some(O::BuiltinCancelDraft), W::EditAcpAgent(index) => Some(O::AcpEdit(index)), W::RemoveAcpAgent(index) => Some(O::AcpRemove(index)), + W::ToggleAcpConnected(index) => Some(O::AcpConnection(index)), W::SaveAcpAgentDraft => Some(O::AcpSaveDraft), W::CancelAcpAgentDraft => Some(O::AcpCancelDraft), W::ToggleMcpServer => Some(O::McpServer), diff --git a/crates/op-host-native/src/widget_host/agent_settings_acp_tests.rs b/crates/op-host-native/src/widget_host/agent_settings_acp_tests.rs index 209b8c94e..524f33896 100644 --- a/crates/op-host-native/src/widget_host/agent_settings_acp_tests.rs +++ b/crates/op-host-native/src/widget_host/agent_settings_acp_tests.rs @@ -1,5 +1,6 @@ use super::WidgetHostNative; use op_editor_core::agent_settings::{AcpAgentField, SettingsFocus}; +use op_editor_core::{AgentSettingsButton, ButtonPressTarget}; use op_editor_ui::widgets::agent_settings_panel::AgentSettingsPanel; fn agent_settings_content_metrics(host: &WidgetHostNative) -> (f32, f32, f32) { @@ -136,14 +137,30 @@ fn acp_agent_connect_press_toggles_connected_state() { let card_y = acp_card_y(content_y); let button_x = content_x + content_w - 60.0; assert!(host.dispatch_agent_settings_press(button_x, card_y + 30.0, 1200.0, 800.0)); + assert_eq!( + host.editor_state().editor_ui.pressed_button, + Some(ButtonPressTarget::AgentSettings( + AgentSettingsButton::AcpConnection(0) + )) + ); assert!( host.editor_state().editor_ui.agent_settings.acp_agents[0].connected, "configured local ACP agent should become connected after pressing Connect" ); + assert!(host.apply_release_with_viewport(1200.0, 800.0)); + assert_eq!(host.editor_state().editor_ui.pressed_button, None); assert!(host.dispatch_agent_settings_press(button_x, card_y + 30.0, 1200.0, 800.0)); + assert_eq!( + host.editor_state().editor_ui.pressed_button, + Some(ButtonPressTarget::AgentSettings( + AgentSettingsButton::AcpConnection(0) + )) + ); assert!( !host.editor_state().editor_ui.agent_settings.acp_agents[0].connected, "connected ACP agent should become disconnected after pressing Disconnect" ); + assert!(host.apply_release_with_viewport(1200.0, 800.0)); + assert_eq!(host.editor_state().editor_ui.pressed_button, None); } diff --git a/crates/op-host-web/src/widget_host.rs b/crates/op-host-web/src/widget_host.rs index 167f640d9..e1a85258d 100644 --- a/crates/op-host-web/src/widget_host.rs +++ b/crates/op-host-web/src/widget_host.rs @@ -43,6 +43,8 @@ use op_editor_ui::{Point2D, Rect, Theme}; mod a11y_bridge; #[cfg(test)] +mod agent_settings_acp_press_tests; +#[cfg(test)] mod agent_settings_compact_press_tests; #[cfg(test)] mod agent_settings_form_press_tests; diff --git a/crates/op-host-web/src/widget_host/agent_settings_acp_press_tests.rs b/crates/op-host-web/src/widget_host/agent_settings_acp_press_tests.rs new file mode 100644 index 000000000..16bbc4243 --- /dev/null +++ b/crates/op-host-web/src/widget_host/agent_settings_acp_press_tests.rs @@ -0,0 +1,43 @@ +use super::WidgetHost; +use op_editor_core::{AgentSettingsButton, ButtonPressTarget}; +use op_editor_ui::widgets::agent_settings_panel::AgentSettingsPanel; + +fn content_metrics(host: &WidgetHost) -> (f32, f32, f32) { + let panel = AgentSettingsPanel::for_editor(&host.editor_state); + let rect = panel.rect(1200.0, 800.0); + ( + rect.origin.x + 200.0 + 24.0, + rect.origin.y + 24.0, + rect.size.x - 200.0 - 48.0, + ) +} + +fn acp_card_y(content_y: f32) -> f32 { + content_y + 12.0 + 120.0 + 28.0 + 28.0 + 28.0 +} + +#[test] +fn acp_agent_connect_press_sets_and_release_clears_agent_settings_button() { + let mut host = WidgetHost::new(); + host.editor_state.editor_ui.agent_settings.add_acp_agent(); + host.editor_state.editor_ui.agent_settings.acp_agents[0].command = "op-agent".into(); + let (content_x, content_y, content_w) = content_metrics(&host); + let button_x = content_x + content_w - 60.0; + + assert!(host.dispatch_agent_settings_press( + button_x, + acp_card_y(content_y) + 30.0, + 1200.0, + 800.0, + )); + assert_eq!( + host.editor_state.editor_ui.pressed_button, + Some(ButtonPressTarget::AgentSettings( + AgentSettingsButton::AcpConnection(0) + )) + ); + assert!(host.editor_state.editor_ui.agent_settings.acp_agents[0].connected); + + assert!(host.apply_release_with_viewport(1200.0, 800.0)); + assert_eq!(host.editor_state.editor_ui.pressed_button, None); +}