refactor(editor): route acp connection button through shared feedback

This commit is contained in:
Kayshen-X 2026-06-14 05:03:18 +08:00
parent fefb3a7be8
commit 7f74c750e2
7 changed files with 135 additions and 10 deletions

View file

@ -13,6 +13,7 @@ pub enum AgentSettingsButton {
BuiltinCancelDraft,
AcpEdit(usize),
AcpRemove(usize),
AcpConnection(usize),
AcpSaveDraft,
AcpCancelDraft,
McpServer,

View file

@ -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() {

View file

@ -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"
);
}

View file

@ -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),

View file

@ -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);
}

View file

@ -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;

View file

@ -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);
}