From 6932081068f5497394f37abd9a1a64dcc2a91536 Mon Sep 17 00:00:00 2001 From: Fini Date: Thu, 30 Jul 2026 08:57:16 +0800 Subject: [PATCH] feat(agent): present custom ACP agents as local integrations --- .../src/widgets/agent_settings_acp.rs | 143 +++---- .../src/widgets/agent_settings_acp_helpers.rs | 4 +- .../src/widgets/agent_settings_builtin.rs | 12 +- .../widgets/agent_settings_builtin_layout.rs | 2 +- .../widgets/agent_settings_header_action.rs | 39 +- .../src/widgets/agent_settings_panel.rs | 2 - .../widgets/agent_settings_panel/hit_test.rs | 6 - .../agent_settings_panel_tests/acp_agents.rs | 390 ++++++++++++++++++ .../builtin_agents.rs | 64 +++ .../widgets/agent_settings_press_entries.rs | 37 +- .../src/widgets/agent_settings_press_focus.rs | 19 +- .../agent_settings_switch_style_tests.rs | 8 +- .../op-editor-ui/src/widgets/settings_form.rs | 9 +- crates/op-i18n/src/i18n/de.rs | 10 +- crates/op-i18n/src/i18n/en.rs | 10 +- crates/op-i18n/src/i18n/es.rs | 10 +- crates/op-i18n/src/i18n/fr.rs | 10 +- crates/op-i18n/src/i18n/hi.rs | 10 +- crates/op-i18n/src/i18n/id.rs | 10 +- crates/op-i18n/src/i18n/ja.rs | 10 +- crates/op-i18n/src/i18n/ko.rs | 8 +- crates/op-i18n/src/i18n/pt.rs | 10 +- crates/op-i18n/src/i18n/ru.rs | 10 +- crates/op-i18n/src/i18n/tests.rs | 17 + crates/op-i18n/src/i18n/th.rs | 10 +- crates/op-i18n/src/i18n/tr.rs | 10 +- crates/op-i18n/src/i18n/vi.rs | 10 +- crates/op-i18n/src/i18n/zh_cn.rs | 8 +- crates/op-i18n/src/i18n/zh_tw.rs | 8 +- 29 files changed, 685 insertions(+), 211 deletions(-) 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 43f8c788e..46ced306d 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_acp.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_acp.rs @@ -2,21 +2,21 @@ use crate::theme::Theme; use crate::widgets::agent_settings_acp_helpers::{ - field_input_rect, form_actions_y, form_card_h, type_toggle_rect, + connection_type_rect, field_input_rect, form_actions_y, form_card_h, }; use crate::widgets::agent_settings_caret::settings_input_text; use crate::widgets::agent_settings_form_actions::{ cancel_button_rect, paint_form_actions, save_button_rect, }; use crate::widgets::agent_settings_header_action::{ - header_action_rect, header_action_text_baseline_y, header_action_text_x, + fit_header_copy, 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, tokens_from_theme}; use crate::widgets::icons::{draw_icon, Icon}; use crate::widgets::settings_form::{self, draw_text, ellipsize, paint_action}; use crate::widgets::PaintCx; -use crate::{Point2D, Rect}; +use crate::{Point2D, Rect, TextLayout}; use jian_widgets::components::button::{Button, ButtonVariant}; use jian_widgets::components::card::Card; use op_editor_core::agent_settings::{ @@ -40,8 +40,6 @@ pub enum AcpHit { AddAgent, Focus { index: usize, field: AcpAgentField }, FocusDraft(AcpAgentField), - ToggleConnectionType(usize), - ToggleDraftConnectionType, SaveDraft, CancelDraft, Edit(usize), @@ -83,9 +81,6 @@ pub fn hit_test(content: Rect, settings: &AgentSettings, point: Point2D, y: f32) card_height(settings, index), ); if is_editing(settings, index) { - if (type_toggle_rect(card)).contains(point) { - return AcpHit::ToggleConnectionType(index); - } for field in form_fields(agent.connection_type) { if (field_input_rect(card, *field)).contains(point) { return AcpHit::Focus { @@ -111,9 +106,6 @@ pub fn hit_test(content: Rect, settings: &AgentSettings, point: Point2D, y: f32) content.size.x, form_card_h(agent.connection_type), ); - if (type_toggle_rect(card)).contains(point) { - return AcpHit::ToggleDraftConnectionType; - } for field in form_fields(agent.connection_type) { if (field_input_rect(card, *field)).contains(point) { return AcpHit::FocusDraft(*field); @@ -172,6 +164,7 @@ pub fn paint_acp_section( t_settings(ui, "settings.agents.acpSubtitle"), content.origin.x, y, + content.size.x, ); if settings.acp_agents.is_empty() && settings.acp_agent_draft.is_none() { return settings_form::paint_empty( @@ -231,23 +224,23 @@ fn paint_header( action_hover: bool, action_pressed: bool, ) -> f32 { + let copy = fit_header_copy(cx, title, action, content.size.x); draw_text( cx, - title, + ©.title, 15.0, theme.foreground, content.origin.x, y + 18.0, ); - let action_w = cx.backend.measure_text(action, 12.0); - let action_rect = header_action_rect(content, y, action_w); + let action_rect = header_action_rect(content, y); paint_ghost_button_feedback(cx.backend, theme, action_rect, action_hover, action_pressed); draw_text( cx, - action, + ©.action, 12.0, theme.primary, - header_action_text_x(action_rect, action_w), + header_action_text_x(action_rect, copy.action_w), header_action_text_baseline_y(action_rect), ); y + HEADER_H @@ -287,7 +280,7 @@ fn paint_compact_acp_card( if connected { theme.accent } else { theme.muted }, ); cx.backend.stroke_round_rect(card, 8.0, theme.border, 1.0); - paint_avatar(cx, theme, ui, agent, card); + paint_avatar(cx, theme, agent, card); let text_x = card.origin.x + 60.0; let name = ellipsize(cx, &agent.display_name, 190.0, 13.0); @@ -371,39 +364,45 @@ fn paint_acp_form( 11.0, theme.muted_foreground, card.origin.x + 12.0, - type_toggle_rect(card).origin.y - 8.0, + connection_type_rect(card).origin.y - 8.0, ); - paint_type_toggle(cx, theme, ui, agent, card); + paint_connection_type_badge(cx, theme, ui, agent, card); for field in form_fields(agent.connection_type) { paint_field(cx, theme, settings, ui, agent, index, *field, card, now_ms); } } -fn paint_avatar( - cx: &mut PaintCx<'_>, - theme: &Theme, - ui: &EditorUiState, - agent: &AcpAgentConfig, - card: Rect, -) { +fn paint_avatar(cx: &mut PaintCx<'_>, theme: &Theme, agent: &AcpAgentConfig, card: Rect) { let avatar = Rect { origin: Point2D::new(card.origin.x + 12.0, card.origin.y + 12.0), size: Point2D::new(36.0, 36.0), }; cx.backend.fill_round_rect(avatar, 8.0, theme.card); - let icon = match agent.connection_type { - AcpConnectionType::Local => Icon::Terminal, - AcpConnectionType::Remote => Icon::Globe, - }; - draw_icon( - cx.backend, - icon, - Point2D::new(avatar.origin.x + 9.0, avatar.origin.y + 9.0), - 18.0, - theme.foreground, - 1.6, + let monogram = agent_monogram(&agent.display_name); + let monogram_w = cx.backend.measure_text(&monogram, 15.0); + let layout = TextLayout::single_run( + &monogram, + "system-ui", + 15.0, + theme.foreground.to_jian(), + Point2D::ZERO, + ) + .with_font_weight(600); + cx.backend.draw_text( + &layout, + Point2D::new( + avatar.origin.x + (avatar.size.x - monogram_w) / 2.0, + jian_widgets::centered_text_baseline_y(avatar, 15.0), + ), ); - let _ = ui; +} + +fn agent_monogram(display_name: &str) -> String { + display_name + .chars() + .find(|ch| !ch.is_whitespace() && !ch.is_control()) + .map(|ch| ch.to_uppercase().collect()) + .unwrap_or_else(|| "?".to_string()) } fn paint_connection_button( @@ -543,51 +542,41 @@ fn paint_field( } } -fn paint_type_toggle( +fn paint_connection_type_badge( cx: &mut PaintCx<'_>, theme: &Theme, ui: &EditorUiState, agent: &AcpAgentConfig, card: Rect, ) { - let r = type_toggle_rect(card); + let r = connection_type_rect(card); + cx.backend.fill_round_rect(r, 6.0, theme.card); cx.backend.stroke_round_rect(r, 6.0, theme.border, 1.0); - let half = r.size.x / 2.0; - for (i, kind) in [AcpConnectionType::Local, AcpConnectionType::Remote] - .iter() - .enumerate() - { - let item = Rect { - origin: Point2D::new(r.origin.x + i as f32 * half, r.origin.y), - size: Point2D::new(half, r.size.y), - }; - let active = agent.connection_type == *kind; - if active { - cx.backend.fill_round_rect(item, 5.0, theme.primary); - } - let color = if active { - theme.primary_foreground - } else { - theme.muted_foreground - }; - let label = connection_type_label(ui, *kind); - let icon = match kind { - AcpConnectionType::Local => Icon::Terminal, - AcpConnectionType::Remote => Icon::Globe, - }; - let tw = cx.backend.measure_text(label, 11.0); - let group_w = 16.0 + 6.0 + tw; - let group_x = item.origin.x + (item.size.x - group_w) / 2.0; - draw_icon( - cx.backend, - icon, - Point2D::new(group_x, item.origin.y + 6.0), - 14.0, - color, - 1.5, - ); - draw_text(cx, label, 11.0, color, group_x + 22.0, item.origin.y + 18.0); - } + let kind = agent.connection_type; + let label = connection_type_label(ui, kind); + let icon = match kind { + AcpConnectionType::Local => Icon::Terminal, + AcpConnectionType::Remote => Icon::Globe, + }; + let tw = cx.backend.measure_text(label, 11.0); + let group_w = 16.0 + 6.0 + tw; + let group_x = r.origin.x + (r.size.x - group_w) / 2.0; + draw_icon( + cx.backend, + icon, + Point2D::new(group_x, r.origin.y + 6.0), + 14.0, + theme.muted_foreground, + 1.5, + ); + draw_text( + cx, + label, + 11.0, + theme.muted_foreground, + group_x + 22.0, + r.origin.y + 18.0, + ); } fn acp_detail(settings: &AgentSettings, agent: &AcpAgentConfig) -> String { @@ -653,7 +642,7 @@ fn card_height(settings: &AgentSettings, index: usize) -> f32 { } fn add_agent_rect(content: Rect, y: f32) -> Rect { - header_action_rect(content, y, 0.0) + header_action_rect(content, y) } fn card_rect(x: f32, y: f32, w: f32, h: f32) -> Rect { diff --git a/crates/op-editor-ui/src/widgets/agent_settings_acp_helpers.rs b/crates/op-editor-ui/src/widgets/agent_settings_acp_helpers.rs index e76e6193a..4bd1f96b7 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_acp_helpers.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_acp_helpers.rs @@ -13,7 +13,9 @@ pub(super) const DRAFT_CARD_H: f32 = 370.0; const FIELD_H: f32 = 28.0; const ENV_FIELD_H: f32 = 64.0; -pub(super) fn type_toggle_rect(card: Rect) -> Rect { +/// Read-only transport badge. New custom-Agent drafts are Local-only in +/// M1; legacy Remote rows keep their persisted label but cannot switch here. +pub(super) fn connection_type_rect(card: Rect) -> Rect { Rect { origin: Point2D::new(card.origin.x + 12.0, card.origin.y + 100.0), size: Point2D::new(card.size.x - 24.0, 28.0), diff --git a/crates/op-editor-ui/src/widgets/agent_settings_builtin.rs b/crates/op-editor-ui/src/widgets/agent_settings_builtin.rs index 9ba620163..4cc1f9c43 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_builtin.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_builtin.rs @@ -12,7 +12,7 @@ use crate::widgets::agent_settings_form_actions::{ cancel_button_rect, paint_form_actions, save_button_rect, }; use crate::widgets::agent_settings_header_action::{ - header_action_rect, header_action_text_baseline_y, header_action_text_x, + fit_header_copy, 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::agent_settings_switch::paint_settings_switch; @@ -284,6 +284,7 @@ pub fn paint_builtin_section( t_settings(ui, "settings.agents.builtinSubtitle"), content.origin.x, y, + content.size.x, ); if let Some(error) = settings.web_credential_sync_error.as_deref() { let text = format!("{} {error}", t_settings(ui, "settings.agents.syncError")); @@ -362,8 +363,9 @@ fn paint_header( action_hover: bool, action_pressed: bool, ) -> f32 { + let copy = fit_header_copy(cx, title, action, frame.w); let layout = TextLayout::single_run( - title, + ©.title, "system-ui", 15.0, (theme.foreground).to_jian(), @@ -371,18 +373,16 @@ fn paint_header( ); cx.backend .draw_text(&layout, Point2D::new(frame.x, frame.y + 18.0)); - let action_w = cx.backend.measure_text(action, 12.0); let action_rect = header_action_rect( Rect { origin: Point2D::new(frame.x, frame.y), size: Point2D::new(frame.w, HEADER_HEIGHT), }, frame.y, - action_w, ); paint_ghost_button_feedback(cx.backend, theme, action_rect, action_hover, action_pressed); let act = TextLayout::single_run( - action, + ©.action, "system-ui", 12.0, (theme.primary).to_jian(), @@ -391,7 +391,7 @@ fn paint_header( cx.backend.draw_text( &act, Point2D::new( - header_action_text_x(action_rect, action_w), + header_action_text_x(action_rect, copy.action_w), header_action_text_baseline_y(action_rect), ), ); diff --git a/crates/op-editor-ui/src/widgets/agent_settings_builtin_layout.rs b/crates/op-editor-ui/src/widgets/agent_settings_builtin_layout.rs index 9f7694d80..654767c41 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_builtin_layout.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_builtin_layout.rs @@ -51,7 +51,7 @@ pub(super) fn draft_card_height(settings: &AgentSettings) -> f32 { } pub(super) fn add_provider_rect(content: Rect, y: f32) -> Rect { - header_action_rect(content, y, 0.0) + header_action_rect(content, y) } pub(super) fn card_rect(x: f32, y: f32, w: f32, h: f32) -> Rect { diff --git a/crates/op-editor-ui/src/widgets/agent_settings_header_action.rs b/crates/op-editor-ui/src/widgets/agent_settings_header_action.rs index 0d0d101da..b88a79ba7 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_header_action.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_header_action.rs @@ -1,18 +1,47 @@ +use crate::widgets::settings_form::ellipsize; +use crate::widgets::PaintCx; use crate::{Point2D, Rect}; pub(super) const HEADER_ACTION_RIGHT_INSET: f32 = 12.0; -const HEADER_ACTION_MIN_W: f32 = 96.0; +const HEADER_ACTION_W: f32 = 208.0; const HEADER_ACTION_PAD_X: f32 = 12.0; const HEADER_ACTION_H: f32 = 24.0; +const HEADER_COPY_GAP: f32 = 12.0; -pub(super) fn header_action_rect(content: Rect, y: f32, text_w: f32) -> Rect { - let w = (text_w + HEADER_ACTION_PAD_X * 2.0).max(HEADER_ACTION_MIN_W); +pub(super) struct FittedHeaderCopy { + pub title: String, + pub action: String, + pub action_w: f32, +} + +/// Keep the section title and trailing action on one fixed-height row without +/// overlap. Long-script locales may need to shorten both sides. +pub(super) fn fit_header_copy( + cx: &mut PaintCx<'_>, + title: &str, + action: &str, + content_w: f32, +) -> FittedHeaderCopy { + let title_max_w = + (content_w - HEADER_ACTION_RIGHT_INSET - HEADER_ACTION_W - HEADER_COPY_GAP).max(0.0); + let title = ellipsize(cx, title, title_max_w, 15.0); + let action_max_w = HEADER_ACTION_W - HEADER_ACTION_PAD_X * 2.0; + let action = ellipsize(cx, action, action_max_w, 12.0); + let action_w = cx.backend.measure_text(&action, 12.0); + FittedHeaderCopy { + title, + action, + action_w, + } +} + +pub(super) fn header_action_rect(content: Rect, y: f32) -> Rect { Rect { origin: Point2D::new( - content.origin.x + content.size.x - HEADER_ACTION_RIGHT_INSET - w, + content.origin.x + content.size.x - HEADER_ACTION_RIGHT_INSET - HEADER_ACTION_W, y, ), - size: Point2D::new(w, HEADER_ACTION_H), + size: Point2D::new(HEADER_ACTION_W, HEADER_ACTION_H), } } diff --git a/crates/op-editor-ui/src/widgets/agent_settings_panel.rs b/crates/op-editor-ui/src/widgets/agent_settings_panel.rs index 193ece95e..7ce2682fe 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_panel.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_panel.rs @@ -140,8 +140,6 @@ pub enum AgentSettingsHit { field: AcpAgentField, }, FocusAcpAgentDraft(AcpAgentField), - ToggleAcpConnectionType(usize), - ToggleAcpDraftConnectionType, SaveAcpAgentDraft, CancelAcpAgentDraft, EditAcpAgent(usize), diff --git a/crates/op-editor-ui/src/widgets/agent_settings_panel/hit_test.rs b/crates/op-editor-ui/src/widgets/agent_settings_panel/hit_test.rs index e6f8ae5b8..cd6f37b18 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_panel/hit_test.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_panel/hit_test.rs @@ -72,12 +72,6 @@ impl AgentSettingsPanel<'_> { AcpHit::FocusDraft(field) => { return AgentSettingsHit::FocusAcpAgentDraft(field) } - AcpHit::ToggleConnectionType(index) => { - return AgentSettingsHit::ToggleAcpConnectionType(index); - } - AcpHit::ToggleDraftConnectionType => { - return AgentSettingsHit::ToggleAcpDraftConnectionType; - } AcpHit::SaveDraft => return AgentSettingsHit::SaveAcpAgentDraft, AcpHit::CancelDraft => return AgentSettingsHit::CancelAcpAgentDraft, AcpHit::Edit(index) => return AgentSettingsHit::EditAcpAgent(index), diff --git a/crates/op-editor-ui/src/widgets/agent_settings_panel_tests/acp_agents.rs b/crates/op-editor-ui/src/widgets/agent_settings_panel_tests/acp_agents.rs index c6a7c5b66..464077a86 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_panel_tests/acp_agents.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_panel_tests/acp_agents.rs @@ -5,6 +5,14 @@ use super::*; +fn acp_form_remote_half_point(content: Rect, section_y: f32) -> Point2D { + let card_y = section_y + 28.0 + 28.0; + Point2D::new( + content.origin.x + 12.0 + (content.size.x - 24.0) * 0.75, + card_y + 100.0 + 14.0, + ) +} + #[test] fn agents_tab_acp_cards_replace_empty_hint_height() { let empty = AgentSettingsPanel::for_editor(&EditorState::default()).content_total_height(); @@ -19,6 +27,388 @@ fn agents_tab_acp_cards_replace_empty_hint_height() { ); } +#[test] +fn full_painted_custom_agent_action_rect_is_clickable_from_its_left_edge() { + let state = EditorState::default(); + let panel = AgentSettingsPanel::for_editor(&state); + let panel_rect = panel.rect(1200.0, 800.0); + let content = crate::widgets::agent_settings_panel_geometry::content_rect(panel_rect); + let section_y = crate::widgets::agent_settings_panel_geometry::acp_section_y( + content, + &state.editor_ui.agent_settings, + ); + let action = + crate::widgets::agent_settings_header_action::header_action_rect(content, section_y); + + assert_eq!( + panel.hit_test( + panel_rect, + Point2D::new(action.origin.x + 1.0, action.origin.y + action.size.y / 2.0), + ), + AgentSettingsHit::AddAcpAgent + ); +} + +#[test] +fn new_local_custom_agent_draft_has_no_visible_or_clickable_remote_choice() { + let mut state = EditorState::default(); + state.editor_ui.agent_settings.begin_acp_agent_draft(); + let content = Rect { + origin: Point2D::new(24.0, 32.0), + size: Point2D::new(520.0, 0.0), + }; + let section_y = 40.0; + + assert_eq!( + crate::widgets::agent_settings_acp::hit_test( + content, + &state.editor_ui.agent_settings, + acp_form_remote_half_point(content, section_y), + section_y, + ), + crate::widgets::agent_settings_acp::AcpHit::None, + "the old Remote half of a new Local draft must be inert" + ); + + let mut backend = CaptureBackend::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + crate::widgets::agent_settings_acp::paint_acp_section( + &mut cx, + &crate::theme::Theme::dark(), + &state.editor_ui.agent_settings, + &state.editor_ui, + content, + section_y, + 0, + ); + let local = op_i18n::translate(state.editor_ui.locale, "acp.local"); + let remote = op_i18n::translate(state.editor_ui.locale, "acp.remote"); + assert!( + backend + .text_effective_points + .iter() + .any(|(text, _)| text == local), + "new custom-Agent drafts should paint a read-only Local badge" + ); + assert!( + !backend + .text_effective_points + .iter() + .any(|(text, _)| text == remote), + "new custom-Agent drafts must not paint a Remote option" + ); +} + +#[test] +fn long_german_and_spanish_acp_subtitles_are_ellipsized_within_content_width() { + let content = Rect { + origin: Point2D::new(24.0, 32.0), + size: Point2D::new(472.0, 0.0), + }; + let section_y = 40.0; + let subtitle_baseline = section_y + 28.0 + 16.0; + + for locale in [op_i18n::Locale::De, op_i18n::Locale::Es] { + let mut state = EditorState::default(); + state.editor_ui.locale = locale; + let mut backend = CaptureBackend::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + crate::widgets::agent_settings_acp::paint_acp_section( + &mut cx, + &crate::theme::Theme::dark(), + &state.editor_ui.agent_settings, + &state.editor_ui, + content, + section_y, + 0, + ); + + let (subtitle, point) = backend + .text_effective_points + .iter() + .find(|(_, point)| (point.y - subtitle_baseline).abs() < 0.01) + .cloned() + .expect("ACP subtitle should be painted on its fixed-height row"); + let painted_w = backend.measure_text(&subtitle, 12.0); + assert!( + subtitle.ends_with("..."), + "{locale:?} ACP subtitle should visibly signal truncation: {subtitle}" + ); + assert!( + point.x + painted_w <= content.origin.x + content.size.x + 0.01, + "{locale:?} ACP subtitle should fit its content row" + ); + } +} + +#[test] +fn localized_acp_header_and_empty_copy_stay_inside_content_width() { + let content = Rect { + origin: Point2D::new(24.0, 32.0), + size: Point2D::new(472.0, 0.0), + }; + + for locale in op_i18n::Locale::ALL { + let mut state = EditorState::default(); + state.editor_ui.locale = locale; + let mut backend = CaptureBackend::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + crate::widgets::agent_settings_acp::paint_acp_section( + &mut cx, + &crate::theme::Theme::dark(), + &state.editor_ui.agent_settings, + &state.editor_ui, + content, + 40.0, + 0, + ); + + assert_eq!( + backend.text_effective_points.len(), + 4, + "{locale:?} empty ACP section should paint title, action, subtitle, and hint" + ); + let rows = backend.text_effective_points.clone(); + for ((text, point), size) in rows.iter().zip([15.0, 12.0, 12.0, 13.0]) { + let painted_w = backend.measure_text(text, size); + assert!( + point.x >= content.origin.x - 0.01 + && point.x + painted_w <= content.origin.x + content.size.x + 0.01, + "{locale:?} text overflows ACP content: {text:?}" + ); + } + let title_right = rows[0].1.x + backend.measure_text(&rows[0].0, 15.0); + assert!( + title_right + 12.0 <= rows[1].1.x + 0.01, + "{locale:?} ACP section title overlaps its add action" + ); + if locale == op_i18n::Locale::De { + assert!( + rows[1].0.ends_with("..."), + "the measured German add action should be safely ellipsized" + ); + } + } +} + +#[test] +fn saved_local_custom_agent_edit_form_cannot_switch_to_remote() { + let mut state = EditorState::default(); + state.editor_ui.agent_settings.add_acp_agent_config( + "Local helper", + op_editor_core::agent_settings::AcpConnectionType::Local, + "op-agent", + Vec::new(), + Default::default(), + None, + true, + ); + state.editor_ui.agent_settings.focus = Some(SettingsFocus::AcpAgent { + index: 0, + field: AcpAgentField::DisplayName, + }); + let content = Rect { + origin: Point2D::new(24.0, 32.0), + size: Point2D::new(520.0, 0.0), + }; + let section_y = 40.0; + + assert_eq!( + crate::widgets::agent_settings_acp::hit_test( + content, + &state.editor_ui.agent_settings, + acp_form_remote_half_point(content, section_y), + section_y, + ), + crate::widgets::agent_settings_acp::AcpHit::None, + "the old Remote half of a saved Local edit form must be inert" + ); +} + +#[test] +fn legacy_remote_custom_agent_keeps_read_only_remote_badge() { + let mut state = EditorState::default(); + state.editor_ui.agent_settings.add_acp_agent_config( + "Legacy remote", + op_editor_core::agent_settings::AcpConnectionType::Remote, + "", + Vec::new(), + Default::default(), + Some("wss://agent.example.com".into()), + true, + ); + state.editor_ui.agent_settings.focus = Some(SettingsFocus::AcpAgent { + index: 0, + field: AcpAgentField::DisplayName, + }); + let content = Rect { + origin: Point2D::new(24.0, 32.0), + size: Point2D::new(520.0, 0.0), + }; + let section_y = 40.0; + + assert_eq!( + crate::widgets::agent_settings_acp::hit_test( + content, + &state.editor_ui.agent_settings, + acp_form_remote_half_point(content, section_y), + section_y, + ), + crate::widgets::agent_settings_acp::AcpHit::None, + "legacy Remote transport should be visible but read-only" + ); + + let mut backend = CaptureBackend::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + crate::widgets::agent_settings_acp::paint_acp_section( + &mut cx, + &crate::theme::Theme::dark(), + &state.editor_ui.agent_settings, + &state.editor_ui, + content, + section_y, + 0, + ); + let remote = op_i18n::translate(state.editor_ui.locale, "acp.remote"); + assert!( + backend + .text_effective_points + .iter() + .any(|(text, _)| text == remote), + "legacy Remote rows should preserve their transport label" + ); +} + +#[test] +fn compact_acp_card_paints_first_displayable_name_character_instead_of_transport_icon() { + let mut state = EditorState::default(); + state.editor_ui.agent_settings.add_acp_agent_config( + " \n自定义助手", + op_editor_core::agent_settings::AcpConnectionType::Local, + "op-agent", + Vec::new(), + Default::default(), + None, + true, + ); + let content = Rect { + origin: Point2D::new(24.0, 32.0), + size: Point2D::new(520.0, 0.0), + }; + let mut backend = CaptureBackend::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + + crate::widgets::agent_settings_acp::paint_acp_section( + &mut cx, + &crate::theme::Theme::dark(), + &state.editor_ui.agent_settings, + &state.editor_ui, + content, + 40.0, + 0, + ); + + assert!( + backend + .text_effective_points + .iter() + .any(|(text, _)| text == "自"), + "compact custom-Agent cards should use a CJK-safe display-name monogram" + ); + assert!( + backend.svg_strokes.is_empty(), + "compact custom-Agent cards should not paint Terminal/Globe transport branding" + ); +} + +#[test] +fn local_custom_agent_draft_still_saves_through_settings_press_flow() { + let mut state = EditorState::default(); + + crate::widgets::agent_settings_press_flow::apply_agent_settings_hit( + &mut state, + AgentSettingsHit::AddAcpAgent, + op_editor_core::host_settings_commit::SettingsCommitScope::Operator, + 100, + ); + let draft = state + .editor_ui + .agent_settings + .acp_agent_draft + .as_ref() + .expect("add-custom-Agent should open a draft"); + assert_eq!( + draft.connection_type, + op_editor_core::agent_settings::AcpConnectionType::Local + ); + + state.editor_ui.settings_input.set_text("op-agent"); + crate::widgets::agent_settings_press_flow::apply_agent_settings_hit( + &mut state, + AgentSettingsHit::SaveAcpAgentDraft, + op_editor_core::host_settings_commit::SettingsCommitScope::Operator, + 101, + ); + + assert!(state.editor_ui.agent_settings.acp_agent_draft.is_none()); + assert_eq!(state.editor_ui.agent_settings.acp_agents.len(), 1); + assert_eq!( + state.editor_ui.agent_settings.acp_agents[0].connection_type, + op_editor_core::agent_settings::AcpConnectionType::Local + ); + assert_eq!( + state.editor_ui.agent_settings.acp_agents[0].command, + "op-agent" + ); +} + +#[test] +fn removing_custom_agent_through_settings_flow_invalidates_runtime_connection_state() { + let mut state = EditorState::default(); + let id = state.editor_ui.agent_settings.add_acp_agent_config( + "Local helper", + op_editor_core::agent_settings::AcpConnectionType::Local, + "op-agent", + Vec::new(), + Default::default(), + None, + true, + ); + state + .editor_ui + .agent_settings + .begin_acp_agent_connect(0) + .expect("configured custom Agent should begin probing"); + + crate::widgets::agent_settings_press_flow::apply_agent_settings_hit( + &mut state, + AgentSettingsHit::RemoveAcpAgent(0), + op_editor_core::host_settings_commit::SettingsCommitScope::Operator, + 102, + ); + + assert!(state.editor_ui.agent_settings.acp_agents.is_empty()); + assert_eq!( + state.editor_ui.agent_settings.pending_acp_agent_connect, + None + ); + assert!(!state + .editor_ui + .agent_settings + .acp_agent_connection + .contains_key(&id)); +} + #[test] fn agents_content_height_contains_every_provider_card() { let state = EditorState::default(); diff --git a/crates/op-editor-ui/src/widgets/agent_settings_panel_tests/builtin_agents.rs b/crates/op-editor-ui/src/widgets/agent_settings_panel_tests/builtin_agents.rs index 63209dc45..a6c6da180 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_panel_tests/builtin_agents.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_panel_tests/builtin_agents.rs @@ -5,6 +5,70 @@ use super::*; +#[test] +fn full_painted_add_provider_action_rect_is_clickable_from_its_left_edge() { + let state = EditorState::default(); + let panel = AgentSettingsPanel::for_editor(&state); + let panel_rect = panel.rect(1200.0, 800.0); + let content = crate::widgets::agent_settings_panel_geometry::content_rect(panel_rect); + let action = crate::widgets::agent_settings_header_action::header_action_rect( + content, + content.origin.y + 12.0, + ); + + assert_eq!( + panel.hit_test( + panel_rect, + Point2D::new(action.origin.x + 1.0, action.origin.y + action.size.y / 2.0), + ), + AgentSettingsHit::AddProvider + ); +} + +#[test] +fn long_spanish_and_russian_builtin_subtitles_fit_the_shared_single_line_row() { + let content = Rect { + origin: Point2D::new(24.0, 32.0), + size: Point2D::new(472.0, 0.0), + }; + let section_y = 40.0; + let subtitle_baseline = section_y + 28.0 + 16.0; + + for locale in [op_i18n::Locale::Es, op_i18n::Locale::Ru] { + let mut state = EditorState::default(); + state.editor_ui.locale = locale; + let mut backend = CaptureBackend::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + crate::widgets::agent_settings_builtin::paint_builtin_section( + &mut cx, + &crate::theme::Theme::dark(), + &state.editor_ui.agent_settings, + &state.editor_ui, + content, + section_y, + 0, + ); + + let (subtitle, point) = backend + .text_effective_points + .iter() + .find(|(_, point)| (point.y - subtitle_baseline).abs() < 0.01) + .cloned() + .expect("built-in subtitle should be painted on its fixed-height row"); + let painted_w = backend.measure_text(&subtitle, 12.0); + assert!( + subtitle.ends_with("..."), + "{locale:?} built-in subtitle should visibly signal truncation" + ); + assert!( + point.x + painted_w <= content.origin.x + content.size.x + 0.01, + "{locale:?} built-in subtitle should fit its content row" + ); + } +} + #[test] fn hit_test_resolves_builtin_agent_api_key_field() { let mut state = EditorState::default(); diff --git a/crates/op-editor-ui/src/widgets/agent_settings_press_entries.rs b/crates/op-editor-ui/src/widgets/agent_settings_press_entries.rs index a076d6a46..59ceb128b 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_press_entries.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_press_entries.rs @@ -246,32 +246,6 @@ pub(crate) fn apply_entry_hit( focus_acp_agent_draft(state, field, now_ms); SettingsPressOutcome::handled() } - AgentSettingsHit::ToggleAcpConnectionType(index) => { - commit(state); - let flipped = state - .editor_ui - .agent_settings - .acp_agents - .get_mut(index) - .map(flip_acp_connection_type); - if let Some(field) = flipped { - focus_acp_agent(state, index, field, now_ms); - } - SettingsPressOutcome::handled() - } - AgentSettingsHit::ToggleAcpDraftConnectionType => { - commit(state); - let flipped = state - .editor_ui - .agent_settings - .acp_agent_draft - .as_mut() - .map(flip_acp_connection_type); - if let Some(field) = flipped { - focus_acp_agent_draft(state, field, now_ms); - } - SettingsPressOutcome::handled() - } AgentSettingsHit::EditAcpAgent(index) => { commit(state); focus_acp_agent(state, index, AcpAgentField::DisplayName, now_ms); @@ -279,9 +253,14 @@ pub(crate) fn apply_entry_hit( } AgentSettingsHit::RemoveAcpAgent(index) => { commit(state); - let agents = &mut state.editor_ui.agent_settings.acp_agents; - if index < agents.len() { - agents.remove(index); + let id = state + .editor_ui + .agent_settings + .acp_agents + .get(index) + .map(|agent| agent.id.clone()); + if let Some(id) = id { + state.editor_ui.agent_settings.remove_acp_agent(&id); clear_focus(state); state.rebuild_chat_models(); } diff --git a/crates/op-editor-ui/src/widgets/agent_settings_press_focus.rs b/crates/op-editor-ui/src/widgets/agent_settings_press_focus.rs index 5a2f07f22..6f35594cb 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_press_focus.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_press_focus.rs @@ -124,8 +124,9 @@ pub(crate) fn acp_field_text( } } -/// The field that carries the transport for `connection_type` — the one -/// a freshly-flipped or unconfigured ACP card focuses. +/// The field that carries the persisted transport for `connection_type` — +/// the one an unconfigured ACP card focuses. New M1 drafts are Local-only; +/// Remote remains here for legacy persisted rows. pub(crate) fn transport_field(connection_type: AcpConnectionType) -> AcpAgentField { match connection_type { AcpConnectionType::Local => AcpAgentField::Command, @@ -133,20 +134,6 @@ pub(crate) fn transport_field(connection_type: AcpConnectionType) -> AcpAgentFie } } -/// Flip Local ↔ Remote on `agent`, drop its connected flag (the live -/// session no longer matches the transport), and report the field to -/// focus next. -pub(crate) fn flip_acp_connection_type( - agent: &mut op_editor_core::agent_settings::AcpAgentConfig, -) -> AcpAgentField { - agent.connection_type = match agent.connection_type { - AcpConnectionType::Local => AcpConnectionType::Remote, - AcpConnectionType::Remote => AcpConnectionType::Local, - }; - agent.connected = false; - transport_field(agent.connection_type) -} - pub(crate) fn focus_acp_agent( state: &mut EditorState, index: usize, diff --git a/crates/op-editor-ui/src/widgets/agent_settings_switch_style_tests.rs b/crates/op-editor-ui/src/widgets/agent_settings_switch_style_tests.rs index 6f370b2d5..0f8d4a2e0 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_switch_style_tests.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_switch_style_tests.rs @@ -125,7 +125,7 @@ fn mcp_server_button_rect(rect: Rect) -> Rect { } } -fn add_provider_button_rect(rect: Rect, text_w: f32) -> Rect { +fn add_provider_button_rect(rect: Rect, _text_w: f32) -> Rect { let (content_x, content_y, content_w) = settings_content_metrics(rect); super::agent_settings_header_action::header_action_rect( Rect { @@ -133,11 +133,10 @@ fn add_provider_button_rect(rect: Rect, text_w: f32) -> Rect { size: Point2D::new(content_w, 0.0), }, content_y + 12.0, - text_w, ) } -fn add_acp_agent_button_rect(rect: Rect, text_w: f32) -> Rect { +fn add_acp_agent_button_rect(rect: Rect, _text_w: f32) -> Rect { let (content_x, content_y, content_w) = settings_content_metrics(rect); let builtin_h = 28.0 + 28.0 + 64.0; let acp_y = content_y + 12.0 + builtin_h + 28.0; @@ -147,7 +146,6 @@ fn add_acp_agent_button_rect(rect: Rect, text_w: f32) -> Rect { size: Point2D::new(content_w, 0.0), }, acp_y, - text_w, ) } @@ -710,7 +708,7 @@ fn builtin_add_provider_text_is_centered_in_hover_wash() { let label = op_i18n::translate(state.editor_ui.locale, "settings.agents.addProvider"); let mut backend = CaptureBackend::default(); let label_w = backend.measure_text(label, 12.0); - let hover_rect = super::agent_settings_header_action::header_action_rect(content, y, label_w); + let hover_rect = super::agent_settings_header_action::header_action_rect(content, y); let expected_x = super::agent_settings_header_action::header_action_text_x(hover_rect, label_w); let expected_y = hover_rect.origin.y + hover_rect.size.y / 2.0 + 4.0; diff --git a/crates/op-editor-ui/src/widgets/settings_form.rs b/crates/op-editor-ui/src/widgets/settings_form.rs index 2e2b5ff8d..9c9e925a6 100644 --- a/crates/op-editor-ui/src/widgets/settings_form.rs +++ b/crates/op-editor-ui/src/widgets/settings_form.rs @@ -49,8 +49,10 @@ pub(crate) fn paint_subtitle( text: &str, x: f32, y: f32, + max_w: f32, ) -> f32 { - draw_text(cx, text, 12.0, theme.muted_foreground, x, y + 16.0); + let shown = ellipsize(cx, text, max_w, 12.0); + draw_text(cx, &shown, 12.0, theme.muted_foreground, x, y + 16.0); y + SUBTITLE_H } @@ -63,10 +65,11 @@ pub(crate) fn paint_empty( y: f32, w: f32, ) -> f32 { - let text_w = cx.backend.measure_text(text, 13.0); + let shown = ellipsize(cx, text, w, 13.0); + let text_w = cx.backend.measure_text(&shown, 13.0); draw_text( cx, - text, + &shown, 13.0, theme.muted_foreground, x + (w - text_w) / 2.0, diff --git a/crates/op-i18n/src/i18n/de.rs b/crates/op-i18n/src/i18n/de.rs index 8e61cf885..34dc7cf5f 100644 --- a/crates/op-i18n/src/i18n/de.rs +++ b/crates/op-i18n/src/i18n/de.rs @@ -552,10 +552,12 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "Keine integrierten Anbieter konfiguriert.", "settings.agents.syncError" => "Server-Synchronisierung fehlgeschlagen:", "settings.agents.addProvider" => "+ Anbieter hinzufügen", - "settings.agents.acp" => "ACP-Agent", - "settings.agents.acpSubtitle" => "Externe ACP-kompatible Agents verbinden.", - "settings.agents.acpEmpty" => "Keine ACP-Agents konfiguriert.", - "settings.agents.addAcp" => "+ Agent hinzufügen", + "settings.agents.acp" => "Allgemeine Integration", + "settings.agents.acpSubtitle" => { + "Jeden ACP-kompatiblen Agent mit einer benutzerdefinierten Konfiguration verbinden." + } + "settings.agents.acpEmpty" => "Keine benutzerdefinierten Agents konfiguriert.", + "settings.agents.addAcp" => "+ Benutzerdefinierten Agent hinzufügen", "settings.agents.title" => "Agents", "settings.agents.connect" => "Verbinden", "settings.agents.disconnect" => "Trennen", diff --git a/crates/op-i18n/src/i18n/en.rs b/crates/op-i18n/src/i18n/en.rs index 7467a12e2..225f12a7a 100644 --- a/crates/op-i18n/src/i18n/en.rs +++ b/crates/op-i18n/src/i18n/en.rs @@ -601,10 +601,12 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "No built-in providers configured.", "settings.agents.syncError" => "Server sync failed:", "settings.agents.addProvider" => "+ Add provider", - "settings.agents.acp" => "ACP Agent", - "settings.agents.acpSubtitle" => "Connect external ACP-compatible Agents.", - "settings.agents.acpEmpty" => "No ACP Agents configured.", - "settings.agents.addAcp" => "+ Add Agent", + "settings.agents.acp" => "Universal integration", + "settings.agents.acpSubtitle" => { + "Connect any ACP-compatible Agent with a custom configuration." + } + "settings.agents.acpEmpty" => "No custom Agents configured.", + "settings.agents.addAcp" => "+ Add custom Agent", "settings.agents.title" => "Agents", "settings.agents.connect" => "Connect", "settings.agents.disconnect" => "Disconnect", diff --git a/crates/op-i18n/src/i18n/es.rs b/crates/op-i18n/src/i18n/es.rs index b98c81c08..160b0d8a5 100644 --- a/crates/op-i18n/src/i18n/es.rs +++ b/crates/op-i18n/src/i18n/es.rs @@ -552,10 +552,12 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "No hay proveedores integrados configurados.", "settings.agents.syncError" => "Error de sincronización con el servidor:", "settings.agents.addProvider" => "+ Añadir proveedor", - "settings.agents.acp" => "Agente ACP", - "settings.agents.acpSubtitle" => "Conecta agentes externos compatibles con ACP.", - "settings.agents.acpEmpty" => "No hay agentes ACP configurados.", - "settings.agents.addAcp" => "+ Añadir agente", + "settings.agents.acp" => "Integración genérica", + "settings.agents.acpSubtitle" => { + "Conecta cualquier Agent compatible con ACP con una configuración personalizada." + } + "settings.agents.acpEmpty" => "No hay Agents personalizados configurados.", + "settings.agents.addAcp" => "+ Añadir Agent personalizado", "settings.agents.title" => "Agentes", "settings.agents.connect" => "Conectar", "settings.agents.disconnect" => "Desconectar", diff --git a/crates/op-i18n/src/i18n/fr.rs b/crates/op-i18n/src/i18n/fr.rs index 4d89736db..7bd725108 100644 --- a/crates/op-i18n/src/i18n/fr.rs +++ b/crates/op-i18n/src/i18n/fr.rs @@ -552,10 +552,12 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "Aucun fournisseur intégré configuré.", "settings.agents.syncError" => "Échec de la synchronisation serveur :", "settings.agents.addProvider" => "+ Ajouter un fournisseur", - "settings.agents.acp" => "Agent ACP", - "settings.agents.acpSubtitle" => "Connectez des agents externes compatibles ACP.", - "settings.agents.acpEmpty" => "Aucun agent ACP configuré.", - "settings.agents.addAcp" => "+ Ajouter un agent", + "settings.agents.acp" => "Intégration générique", + "settings.agents.acpSubtitle" => { + "Connectez tout Agent compatible ACP avec une configuration personnalisée." + } + "settings.agents.acpEmpty" => "Aucun Agent personnalisé configuré.", + "settings.agents.addAcp" => "+ Ajouter un Agent personnalisé", "settings.agents.title" => "Agents", "settings.agents.connect" => "Connecter", "settings.agents.disconnect" => "Déconnecter", diff --git a/crates/op-i18n/src/i18n/hi.rs b/crates/op-i18n/src/i18n/hi.rs index e8970b96c..831ba6dcf 100644 --- a/crates/op-i18n/src/i18n/hi.rs +++ b/crates/op-i18n/src/i18n/hi.rs @@ -550,10 +550,12 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "कोई अंतर्निहित प्रदाता कॉन्फ़िगर नहीं किया गया।", "settings.agents.syncError" => "सर्वर सिंक विफल:", "settings.agents.addProvider" => "+ प्रदाता जोड़ें", - "settings.agents.acp" => "ACP एजेंट", - "settings.agents.acpSubtitle" => "बाहरी ACP-संगत एजेंट कनेक्ट करें।", - "settings.agents.acpEmpty" => "कोई ACP एजेंट कॉन्फ़िगर नहीं किया गया।", - "settings.agents.addAcp" => "+ एजेंट जोड़ें", + "settings.agents.acp" => "सामान्य इंटीग्रेशन", + "settings.agents.acpSubtitle" => { + "कस्टम कॉन्फ़िगरेशन से किसी भी ACP-संगत Agent को कनेक्ट करें।" + } + "settings.agents.acpEmpty" => "कोई कस्टम Agent कॉन्फ़िगर नहीं है।", + "settings.agents.addAcp" => "+ कस्टम Agent जोड़ें", "settings.agents.title" => "एजेंट", "settings.agents.connect" => "कनेक्ट करें", "settings.agents.disconnect" => "डिस्कनेक्ट करें", diff --git a/crates/op-i18n/src/i18n/id.rs b/crates/op-i18n/src/i18n/id.rs index 20f443e68..2b4dcc550 100644 --- a/crates/op-i18n/src/i18n/id.rs +++ b/crates/op-i18n/src/i18n/id.rs @@ -550,10 +550,12 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "Tidak ada penyedia bawaan yang dikonfigurasi.", "settings.agents.syncError" => "Sinkronisasi server gagal:", "settings.agents.addProvider" => "+ Tambah penyedia", - "settings.agents.acp" => "Agent ACP", - "settings.agents.acpSubtitle" => "Hubungkan Agent eksternal yang kompatibel dengan ACP.", - "settings.agents.acpEmpty" => "Tidak ada Agent ACP yang dikonfigurasi.", - "settings.agents.addAcp" => "+ Tambah Agent", + "settings.agents.acp" => "Integrasi umum", + "settings.agents.acpSubtitle" => { + "Hubungkan Agent apa pun yang kompatibel dengan ACP menggunakan konfigurasi khusus." + } + "settings.agents.acpEmpty" => "Tidak ada Agent khusus yang dikonfigurasi.", + "settings.agents.addAcp" => "+ Tambah Agent khusus", "settings.agents.title" => "Agents", "settings.agents.connect" => "Hubungkan", "settings.agents.disconnect" => "Putuskan", diff --git a/crates/op-i18n/src/i18n/ja.rs b/crates/op-i18n/src/i18n/ja.rs index bb727060d..bb27cb589 100644 --- a/crates/op-i18n/src/i18n/ja.rs +++ b/crates/op-i18n/src/i18n/ja.rs @@ -552,10 +552,12 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "組み込みプロバイダーが設定されていません。", "settings.agents.syncError" => "サーバー同期に失敗しました:", "settings.agents.addProvider" => "+ プロバイダーを追加", - "settings.agents.acp" => "ACP エージェント", - "settings.agents.acpSubtitle" => "外部の ACP 対応エージェントに接続します。", - "settings.agents.acpEmpty" => "ACP エージェントが設定されていません。", - "settings.agents.addAcp" => "+ エージェントを追加", + "settings.agents.acp" => "汎用連携", + "settings.agents.acpSubtitle" => { + "カスタム設定で任意の ACP 対応 Agent を接続します。" + } + "settings.agents.acpEmpty" => "カスタム Agent は設定されていません。", + "settings.agents.addAcp" => "+ カスタム Agent を追加", "settings.agents.title" => "Agents", "settings.agents.connect" => "接続", "settings.agents.disconnect" => "切断", diff --git a/crates/op-i18n/src/i18n/ko.rs b/crates/op-i18n/src/i18n/ko.rs index 4e223d143..13994f61c 100644 --- a/crates/op-i18n/src/i18n/ko.rs +++ b/crates/op-i18n/src/i18n/ko.rs @@ -550,10 +550,10 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "구성된 내장 제공자가 없습니다.", "settings.agents.syncError" => "서버 동기화 실패:", "settings.agents.addProvider" => "+ 제공자 추가", - "settings.agents.acp" => "ACP 에이전트", - "settings.agents.acpSubtitle" => "외부 ACP 호환 에이전트를 연결합니다.", - "settings.agents.acpEmpty" => "구성된 ACP 에이전트가 없습니다.", - "settings.agents.addAcp" => "+ 에이전트 추가", + "settings.agents.acp" => "범용 연동", + "settings.agents.acpSubtitle" => "사용자 지정 구성으로 모든 ACP 호환 Agent를 연결합니다.", + "settings.agents.acpEmpty" => "구성된 사용자 지정 Agent가 없습니다.", + "settings.agents.addAcp" => "+ 사용자 지정 Agent 추가", "settings.agents.title" => "에이전트", "settings.agents.connect" => "연결", "settings.agents.disconnect" => "연결 해제", diff --git a/crates/op-i18n/src/i18n/pt.rs b/crates/op-i18n/src/i18n/pt.rs index f6c943cbd..461ed4ab1 100644 --- a/crates/op-i18n/src/i18n/pt.rs +++ b/crates/op-i18n/src/i18n/pt.rs @@ -552,10 +552,12 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "Nenhum provedor integrado configurado.", "settings.agents.syncError" => "Falha na sincronização com o servidor:", "settings.agents.addProvider" => "+ Adicionar provedor", - "settings.agents.acp" => "Agente ACP", - "settings.agents.acpSubtitle" => "Conecte agentes externos compatíveis com ACP.", - "settings.agents.acpEmpty" => "Nenhum agente ACP configurado.", - "settings.agents.addAcp" => "+ Adicionar agente", + "settings.agents.acp" => "Integração genérica", + "settings.agents.acpSubtitle" => { + "Conecte qualquer Agent compatível com ACP usando uma configuração personalizada." + } + "settings.agents.acpEmpty" => "Nenhum Agent personalizado configurado.", + "settings.agents.addAcp" => "+ Adicionar Agent personalizado", "settings.agents.title" => "Agentes", "settings.agents.connect" => "Conectar", "settings.agents.disconnect" => "Desconectar", diff --git a/crates/op-i18n/src/i18n/ru.rs b/crates/op-i18n/src/i18n/ru.rs index 248855321..de68b8535 100644 --- a/crates/op-i18n/src/i18n/ru.rs +++ b/crates/op-i18n/src/i18n/ru.rs @@ -552,10 +552,12 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "Встроенные провайдеры не настроены.", "settings.agents.syncError" => "Ошибка синхронизации с сервером:", "settings.agents.addProvider" => "+ Добавить провайдера", - "settings.agents.acp" => "Агент ACP", - "settings.agents.acpSubtitle" => "Подключение внешних ACP-совместимых агентов.", - "settings.agents.acpEmpty" => "Агенты ACP не настроены.", - "settings.agents.addAcp" => "+ Добавить агента", + "settings.agents.acp" => "Универсальное подключение", + "settings.agents.acpSubtitle" => { + "Подключайте любые ACP-совместимые Agent с помощью пользовательской конфигурации." + } + "settings.agents.acpEmpty" => "Пользовательские Agent не настроены.", + "settings.agents.addAcp" => "+ Добавить пользовательский Agent", "settings.agents.title" => "Агенты", "settings.agents.connect" => "Подключить", "settings.agents.disconnect" => "Отключить", diff --git a/crates/op-i18n/src/i18n/tests.rs b/crates/op-i18n/src/i18n/tests.rs index 1ec096f1f..257c96852 100644 --- a/crates/op-i18n/src/i18n/tests.rs +++ b/crates/op-i18n/src/i18n/tests.rs @@ -12,6 +12,23 @@ fn en_us_returns_english_chrome_strings() { assert_eq!(translate(Locale::EnUs, "common.untitled"), "Untitled"); } +#[test] +fn custom_agent_entry_is_presented_as_a_universal_integration() { + assert_eq!(translate(Locale::ZhCn, "settings.agents.acp"), "通用接入"); + assert_eq!( + translate(Locale::ZhCn, "settings.agents.addAcp"), + "+ 添加自定义 Agent" + ); + assert_eq!( + translate(Locale::EnUs, "settings.agents.acp"), + "Universal integration" + ); + assert_eq!( + translate(Locale::EnUs, "settings.agents.addAcp"), + "+ Add custom Agent" + ); +} + #[test] fn every_locale_has_a_direct_common_translation() { for locale in Locale::ALL { diff --git a/crates/op-i18n/src/i18n/th.rs b/crates/op-i18n/src/i18n/th.rs index e0087af54..c1f3f80c6 100644 --- a/crates/op-i18n/src/i18n/th.rs +++ b/crates/op-i18n/src/i18n/th.rs @@ -552,10 +552,12 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "ยังไม่ได้กำหนดค่าผู้ให้บริการในตัว", "settings.agents.syncError" => "การซิงค์กับเซิร์ฟเวอร์ล้มเหลว:", "settings.agents.addProvider" => "+ เพิ่มผู้ให้บริการ", - "settings.agents.acp" => "ACP Agent", - "settings.agents.acpSubtitle" => "เชื่อมต่อ Agent ภายนอกที่รองรับ ACP", - "settings.agents.acpEmpty" => "ยังไม่ได้กำหนดค่า ACP Agent", - "settings.agents.addAcp" => "+ เพิ่ม Agent", + "settings.agents.acp" => "การเชื่อมต่อทั่วไป", + "settings.agents.acpSubtitle" => { + "เชื่อมต่อ Agent ที่รองรับ ACP ด้วยการกำหนดค่าแบบกำหนดเอง" + } + "settings.agents.acpEmpty" => "ยังไม่ได้กำหนดค่า Agent แบบกำหนดเอง", + "settings.agents.addAcp" => "+ เพิ่ม Agent แบบกำหนดเอง", "settings.agents.title" => "Agents", "settings.agents.connect" => "เชื่อมต่อ", "settings.agents.disconnect" => "ยกเลิกการเชื่อมต่อ", diff --git a/crates/op-i18n/src/i18n/tr.rs b/crates/op-i18n/src/i18n/tr.rs index 6461d51c3..3ce7b74b5 100644 --- a/crates/op-i18n/src/i18n/tr.rs +++ b/crates/op-i18n/src/i18n/tr.rs @@ -552,10 +552,12 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "Yapılandırılmış yerleşik sağlayıcı yok.", "settings.agents.syncError" => "Sunucu eşitlemesi başarısız:", "settings.agents.addProvider" => "+ Sağlayıcı ekle", - "settings.agents.acp" => "ACP Aracısı", - "settings.agents.acpSubtitle" => "Harici ACP uyumlu aracıları bağlayın.", - "settings.agents.acpEmpty" => "Yapılandırılmış ACP aracısı yok.", - "settings.agents.addAcp" => "+ Aracı ekle", + "settings.agents.acp" => "Genel entegrasyon", + "settings.agents.acpSubtitle" => { + "ACP uyumlu herhangi bir Agent'ı özel bir yapılandırmayla bağlayın." + } + "settings.agents.acpEmpty" => "Yapılandırılmış özel Agent yok.", + "settings.agents.addAcp" => "+ Özel Agent ekle", "settings.agents.title" => "Ajanlar", "settings.agents.connect" => "Bağlan", "settings.agents.disconnect" => "Bağlantıyı kes", diff --git a/crates/op-i18n/src/i18n/vi.rs b/crates/op-i18n/src/i18n/vi.rs index dcb1fd541..80d4ed3ac 100644 --- a/crates/op-i18n/src/i18n/vi.rs +++ b/crates/op-i18n/src/i18n/vi.rs @@ -552,10 +552,12 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "Chưa cấu hình nhà cung cấp tích hợp nào.", "settings.agents.syncError" => "Đồng bộ máy chủ thất bại:", "settings.agents.addProvider" => "+ Thêm nhà cung cấp", - "settings.agents.acp" => "Agent ACP", - "settings.agents.acpSubtitle" => "Kết nối các Agent bên ngoài tương thích ACP.", - "settings.agents.acpEmpty" => "Chưa cấu hình Agent ACP nào.", - "settings.agents.addAcp" => "+ Thêm Agent", + "settings.agents.acp" => "Tích hợp chung", + "settings.agents.acpSubtitle" => { + "Kết nối bất kỳ Agent tương thích ACP nào bằng cấu hình tùy chỉnh." + } + "settings.agents.acpEmpty" => "Chưa cấu hình Agent tùy chỉnh nào.", + "settings.agents.addAcp" => "+ Thêm Agent tùy chỉnh", "settings.agents.title" => "Agents", "settings.agents.connect" => "Kết nối", "settings.agents.disconnect" => "Ngắt kết nối", diff --git a/crates/op-i18n/src/i18n/zh_cn.rs b/crates/op-i18n/src/i18n/zh_cn.rs index 5a8b5ffa5..af4190067 100644 --- a/crates/op-i18n/src/i18n/zh_cn.rs +++ b/crates/op-i18n/src/i18n/zh_cn.rs @@ -615,10 +615,10 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "尚未配置内置服务商。", "settings.agents.syncError" => "服务端同步失败:", "settings.agents.addProvider" => "+ 添加服务商", - "settings.agents.acp" => "ACP Agent", - "settings.agents.acpSubtitle" => "连接外部 ACP 兼容的 Agent。", - "settings.agents.acpEmpty" => "尚未配置 ACP Agent。", - "settings.agents.addAcp" => "+ 添加 Agent", + "settings.agents.acp" => "通用接入", + "settings.agents.acpSubtitle" => "通过自定义配置接入任意 ACP 兼容 Agent。", + "settings.agents.acpEmpty" => "尚未配置自定义 Agent。", + "settings.agents.addAcp" => "+ 添加自定义 Agent", "settings.agents.title" => "Agents", "settings.agents.connect" => "连接", "settings.agents.disconnect" => "断开连接", diff --git a/crates/op-i18n/src/i18n/zh_tw.rs b/crates/op-i18n/src/i18n/zh_tw.rs index 18d3ffba9..c18cbdf1a 100644 --- a/crates/op-i18n/src/i18n/zh_tw.rs +++ b/crates/op-i18n/src/i18n/zh_tw.rs @@ -567,10 +567,10 @@ pub fn lookup(key: &str) -> Option<&'static str> { "settings.agents.builtinEmpty" => "尚未設定內建服務商。", "settings.agents.syncError" => "伺服器同步失敗:", "settings.agents.addProvider" => "+ 新增服務商", - "settings.agents.acp" => "ACP Agent", - "settings.agents.acpSubtitle" => "連接外部 ACP 相容的 Agent。", - "settings.agents.acpEmpty" => "尚未設定 ACP Agent。", - "settings.agents.addAcp" => "+ 新增 Agent", + "settings.agents.acp" => "通用接入", + "settings.agents.acpSubtitle" => "透過自訂設定接入任意 ACP 相容 Agent。", + "settings.agents.acpEmpty" => "尚未設定自訂 Agent。", + "settings.agents.addAcp" => "+ 新增自訂 Agent", "settings.agents.title" => "Agents", "settings.agents.connect" => "連接", "settings.agents.disconnect" => "中斷連線",