feat(agent): present custom ACP agents as local integrations
This commit is contained in:
parent
ddac26e23a
commit
6932081068
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -140,8 +140,6 @@ pub enum AgentSettingsHit {
|
|||
field: AcpAgentField,
|
||||
},
|
||||
FocusAcpAgentDraft(AcpAgentField),
|
||||
ToggleAcpConnectionType(usize),
|
||||
ToggleAcpDraftConnectionType,
|
||||
SaveAcpAgentDraft,
|
||||
CancelAcpAgentDraft,
|
||||
EditAcpAgent(usize),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" => "डिस्कनेक्ट करें",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" => "切断",
|
||||
|
|
|
|||
|
|
@ -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" => "연결 해제",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" => "Отключить",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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" => "ยกเลิกการเชื่อมต่อ",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" => "断开连接",
|
||||
|
|
|
|||
|
|
@ -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" => "中斷連線",
|
||||
|
|
|
|||
Loading…
Reference in a new issue