feat(agent): present custom ACP agents as local integrations

This commit is contained in:
Fini 2026-07-30 08:57:16 +08:00
parent ddac26e23a
commit 6932081068
29 changed files with 685 additions and 211 deletions

View file

@ -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,
&copy.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,
&copy.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 {

View file

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

View file

@ -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,
&copy.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,
&copy.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),
),
);

View file

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

View file

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

View file

@ -140,8 +140,6 @@ pub enum AgentSettingsHit {
field: AcpAgentField,
},
FocusAcpAgentDraft(AcpAgentField),
ToggleAcpConnectionType(usize),
ToggleAcpDraftConnectionType,
SaveAcpAgentDraft,
CancelAcpAgentDraft,
EditAcpAgent(usize),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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" => "डिस्कनेक्ट करें",

View file

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

View file

@ -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" => "切断",

View file

@ -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" => "연결 해제",

View file

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

View file

@ -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" => "Отключить",

View file

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

View file

@ -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" => "ยกเลิกการเชื่อมต่อ",

View file

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

View file

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

View file

@ -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" => "断开连接",

View file

@ -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" => "中斷連線",