fix(agent): keep pure provider base urls read-only

This commit is contained in:
Fini 2026-05-31 22:30:29 +08:00
parent 21a92e5740
commit 048a786fa5
10 changed files with 224 additions and 119 deletions

View file

@ -191,6 +191,13 @@ pub struct BuiltinAgentConfig {
}
impl BuiltinAgentConfig {
pub fn base_url_editable(&self) -> bool {
!matches!(
self.preset,
BuiltinAgentPresetKey::Anthropic | BuiltinAgentPresetKey::OpenAi
)
}
pub fn ready(&self) -> bool {
self.enabled && !self.api_key.trim().is_empty() && !self.model.trim().is_empty()
}

View file

@ -156,6 +156,19 @@ fn pure_builtin_presets_do_not_toggle_api_format() {
assert_eq!(openai.base_url, "https://api.openai.com/v1");
}
#[test]
fn pure_builtin_presets_keep_base_url_read_only_like_ts() {
let mut s = AgentSettings::default();
for _ in 0..3 {
s.add_builtin_agent();
}
assert!(!s.builtin_agents[0].base_url_editable());
assert!(!s.builtin_agents[1].base_url_editable());
assert!(s.builtin_agents[2].base_url_editable());
}
#[test]
fn add_acp_agent_assigns_id_and_defaults_to_local_config() {
let mut s = AgentSettings::default();

View file

@ -2,6 +2,11 @@
use crate::theme::Theme;
use crate::widgets::agent_settings_builtin_draft;
use crate::widgets::agent_settings_builtin_layout::{
add_provider_rect, card_height, card_rect, compact_edit_rect, compact_remove_rect,
compact_switch_rect, draft_card_height, expanded_card_height, field_input_rect, is_editing,
rect_contains, CARD_GAP, EMPTY_HEIGHT, HEADER_HEIGHT, SUBTITLE_HEIGHT, TOP_HEADER_RIGHT_INSET,
};
use crate::widgets::agent_settings_builtin_parts;
use crate::widgets::agent_settings_caret::{
caret_x_for_text, paint_caret, settings_caret_for_focus,
@ -21,21 +26,6 @@ use op_editor_core::agent_settings::{
use op_editor_core::editor_ui_state::EditorUiState;
use op_editor_core::BuiltinAgentPresetKey;
const HEADER_HEIGHT: f32 = 28.0;
const SUBTITLE_HEIGHT: f32 = 28.0;
const EMPTY_HEIGHT: f32 = 64.0;
const COMPACT_CARD_HEIGHT: f32 = 60.0;
const EXPANDED_CARD_HEIGHT: f32 = 196.0;
const DRAFT_ACTION_HEIGHT: f32 = 36.0;
const CARD_GAP: f32 = 8.0;
const ADD_W: f32 = 96.0;
const TOP_HEADER_RIGHT_INSET: f32 = 12.0;
const FIELD_LABEL_W: f32 = 68.0;
const FIELD_H: f32 = 24.0;
const SWITCH_W: f32 = 34.0;
const SWITCH_H: f32 = 20.0;
const ACTION_W: f32 = 24.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuiltinHit {
AddProvider,
@ -125,6 +115,9 @@ pub fn hit_test(content: Rect, settings: &AgentSettings, point: Point2D) -> Buil
.into_iter()
.enumerate()
{
if field == BuiltinAgentField::BaseUrl && !agent.base_url_editable() {
continue;
}
if rect_contains(field_input_rect(settings, card, Some(index), row), point) {
return BuiltinHit::Focus { index, field };
}
@ -179,6 +172,9 @@ pub fn hit_test(content: Rect, settings: &AgentSettings, point: Point2D) -> Buil
.into_iter()
.enumerate()
{
if field == BuiltinAgentField::BaseUrl && !agent.base_url_editable() {
continue;
}
if rect_contains(field_input_rect(settings, card, None, row), point) {
return BuiltinHit::FocusDraft(field);
}
@ -632,6 +628,7 @@ fn paint_field(
BuiltinAgentField::BaseUrl => "Base URL",
};
let input = field_input_rect(settings, card, index, row);
let editable = field != BuiltinAgentField::BaseUrl || agent.base_url_editable();
let label_y = input.origin.y + 16.0;
draw_text(
cx,
@ -662,7 +659,11 @@ fn paint_field(
cx,
&clipped,
11.0,
theme.foreground,
if editable {
theme.foreground
} else {
theme.muted_foreground
},
text_x,
input.origin.y + 16.0,
);
@ -749,99 +750,6 @@ fn mask_key(api_key: &str) -> String {
}
}
fn is_editing(settings: &AgentSettings, index: usize) -> bool {
matches!(
settings.focus,
Some(SettingsFocus::BuiltinAgent { index: i, .. }) if i == index
)
}
fn card_height(settings: &AgentSettings, index: usize) -> f32 {
if is_editing(settings, index) {
expanded_card_height(settings, Some(index))
} else {
COMPACT_CARD_HEIGHT
}
}
fn expanded_card_height(settings: &AgentSettings, index: Option<usize>) -> f32 {
EXPANDED_CARD_HEIGHT + agent_settings_builtin_parts::preset_menu_height(settings, index)
}
fn draft_card_height(settings: &AgentSettings) -> f32 {
expanded_card_height(settings, None) + DRAFT_ACTION_HEIGHT
}
fn add_provider_rect(content: Rect, y: f32) -> Rect {
Rect {
origin: Point2D::new(
content.origin.x + content.size.x - TOP_HEADER_RIGHT_INSET - ADD_W,
y,
),
size: Point2D::new(ADD_W, 24.0),
}
}
fn card_rect(x: f32, y: f32, w: f32, h: f32) -> Rect {
Rect {
origin: Point2D::new(x, y),
size: Point2D::new(w, h),
}
}
fn compact_switch_rect(card: Rect) -> Rect {
Rect {
origin: Point2D::new(
card.origin.x + card.size.x - 12.0 - SWITCH_W - 8.0 - ACTION_W * 2.0 - 4.0,
card.origin.y + (card.size.y - SWITCH_H) / 2.0,
),
size: Point2D::new(SWITCH_W, SWITCH_H),
}
}
fn compact_edit_rect(card: Rect) -> Rect {
Rect {
origin: Point2D::new(
compact_switch_rect(card).origin.x + SWITCH_W + 8.0,
card.origin.y + (card.size.y - ACTION_W) / 2.0,
),
size: Point2D::new(ACTION_W, ACTION_W),
}
}
fn compact_remove_rect(card: Rect) -> Rect {
Rect {
origin: Point2D::new(
compact_edit_rect(card).origin.x + ACTION_W + 4.0,
card.origin.y + (card.size.y - ACTION_W) / 2.0,
),
size: Point2D::new(ACTION_W, ACTION_W),
}
}
fn field_input_rect(
settings: &AgentSettings,
card: Rect,
index: Option<usize>,
row: usize,
) -> Rect {
let menu_h = agent_settings_builtin_parts::preset_menu_height(settings, index);
Rect {
origin: Point2D::new(
card.origin.x + 12.0 + FIELD_LABEL_W,
card.origin.y + 76.0 + menu_h + row as f32 * 28.0,
),
size: Point2D::new(card.size.x - 24.0 - FIELD_LABEL_W, FIELD_H),
}
}
fn rect_contains(r: Rect, p: Point2D) -> bool {
p.x >= r.origin.x
&& p.y >= r.origin.y
&& p.x <= r.origin.x + r.size.x
&& p.y <= r.origin.y + r.size.y
}
fn to_jian(c: Color) -> jian_core::scene::Color {
fn ch(v: f32) -> u8 {
(v.clamp(0.0, 1.0) * 255.0).round() as u8

View file

@ -0,0 +1,111 @@
use crate::widgets::agent_settings_builtin_parts;
use crate::{Point2D, Rect};
use op_editor_core::agent_settings::{AgentSettings, SettingsFocus};
pub(super) const HEADER_HEIGHT: f32 = 28.0;
pub(super) const SUBTITLE_HEIGHT: f32 = 28.0;
pub(super) const EMPTY_HEIGHT: f32 = 64.0;
const COMPACT_CARD_HEIGHT: f32 = 60.0;
const EXPANDED_CARD_HEIGHT: f32 = 196.0;
const DRAFT_ACTION_HEIGHT: f32 = 36.0;
pub(super) const CARD_GAP: f32 = 8.0;
const ADD_W: f32 = 96.0;
pub(super) const TOP_HEADER_RIGHT_INSET: f32 = 12.0;
const FIELD_LABEL_W: f32 = 68.0;
const FIELD_H: f32 = 24.0;
const SWITCH_W: f32 = 34.0;
const SWITCH_H: f32 = 20.0;
const ACTION_W: f32 = 24.0;
pub(super) fn is_editing(settings: &AgentSettings, index: usize) -> bool {
matches!(
settings.focus,
Some(SettingsFocus::BuiltinAgent { index: i, .. }) if i == index
)
}
pub(super) fn card_height(settings: &AgentSettings, index: usize) -> f32 {
if is_editing(settings, index) {
expanded_card_height(settings, Some(index))
} else {
COMPACT_CARD_HEIGHT
}
}
pub(super) fn expanded_card_height(settings: &AgentSettings, index: Option<usize>) -> f32 {
EXPANDED_CARD_HEIGHT + agent_settings_builtin_parts::preset_menu_height(settings, index)
}
pub(super) fn draft_card_height(settings: &AgentSettings) -> f32 {
expanded_card_height(settings, None) + DRAFT_ACTION_HEIGHT
}
pub(super) fn add_provider_rect(content: Rect, y: f32) -> Rect {
Rect {
origin: Point2D::new(
content.origin.x + content.size.x - TOP_HEADER_RIGHT_INSET - ADD_W,
y,
),
size: Point2D::new(ADD_W, 24.0),
}
}
pub(super) fn card_rect(x: f32, y: f32, w: f32, h: f32) -> Rect {
Rect {
origin: Point2D::new(x, y),
size: Point2D::new(w, h),
}
}
pub(super) fn compact_switch_rect(card: Rect) -> Rect {
Rect {
origin: Point2D::new(
card.origin.x + card.size.x - 12.0 - SWITCH_W - 8.0 - ACTION_W * 2.0 - 4.0,
card.origin.y + (card.size.y - SWITCH_H) / 2.0,
),
size: Point2D::new(SWITCH_W, SWITCH_H),
}
}
pub(super) fn compact_edit_rect(card: Rect) -> Rect {
Rect {
origin: Point2D::new(
compact_switch_rect(card).origin.x + SWITCH_W + 8.0,
card.origin.y + (card.size.y - ACTION_W) / 2.0,
),
size: Point2D::new(ACTION_W, ACTION_W),
}
}
pub(super) fn compact_remove_rect(card: Rect) -> Rect {
Rect {
origin: Point2D::new(
compact_edit_rect(card).origin.x + ACTION_W + 4.0,
card.origin.y + (card.size.y - ACTION_W) / 2.0,
),
size: Point2D::new(ACTION_W, ACTION_W),
}
}
pub(super) fn field_input_rect(
settings: &AgentSettings,
card: Rect,
index: Option<usize>,
row: usize,
) -> Rect {
let menu_h = agent_settings_builtin_parts::preset_menu_height(settings, index);
Rect {
origin: Point2D::new(
card.origin.x + 12.0 + FIELD_LABEL_W,
card.origin.y + 76.0 + menu_h + row as f32 * 28.0,
),
size: Point2D::new(card.size.x - 24.0 - FIELD_LABEL_W, FIELD_H),
}
}
pub(super) fn rect_contains(r: Rect, p: Point2D) -> bool {
p.x >= r.origin.x
&& p.y >= r.origin.y
&& p.x <= r.origin.x + r.size.x
&& p.y <= r.origin.y + r.size.y
}

View file

@ -0,0 +1,23 @@
use crate::widgets::agent_settings_panel::{AgentSettingsHit, AgentSettingsPanel};
use crate::Point2D;
use op_editor_core::agent_settings::{BuiltinAgentField, SettingsFocus};
use op_editor_core::EditorState;
#[test]
fn pure_builtin_provider_base_url_is_read_only_hit_target() {
let mut state = EditorState::default();
state.editor_ui.agent_settings.add_builtin_agent();
state.editor_ui.agent_settings.focus = Some(SettingsFocus::BuiltinAgent {
index: 0,
field: BuiltinAgentField::ApiKey,
});
let panel = AgentSettingsPanel::for_editor(&state);
let rect = panel.rect(1200.0, 800.0);
let content_x = rect.origin.x + 200.0 + 24.0;
let content_y = rect.origin.y + 24.0;
let first_card_y = content_y + 12.0 + 28.0 + 28.0;
let point = Point2D::new(content_x + 92.0, first_card_y + 170.0);
assert_eq!(panel.hit_test(rect, point), AgentSettingsHit::Inside);
}

View file

@ -101,7 +101,10 @@ pub mod agent_settings_acp;
mod agent_settings_acp_draft;
pub mod agent_settings_builtin;
mod agent_settings_builtin_draft;
mod agent_settings_builtin_layout;
mod agent_settings_builtin_parts;
#[cfg(test)]
mod agent_settings_builtin_tests;
mod agent_settings_caret;
mod agent_settings_form_actions;
pub mod agent_settings_i18n;

View file

@ -15,6 +15,9 @@ impl WidgetHostNative {
.builtin_agent_draft
.as_ref()
{
if field == BuiltinAgentField::BaseUrl && !agent.base_url_editable() {
return;
}
self.editor_state.editor_ui.settings_input_draft = match field {
BuiltinAgentField::DisplayName => agent.display_name.clone(),
BuiltinAgentField::ApiKey => agent.api_key.clone(),

View file

@ -176,6 +176,34 @@ fn builtin_agent_kind_toggle_commits_focused_api_key_draft() {
assert!(host.editor_state().editor_ui.agent_settings.focus.is_none());
}
#[test]
fn pure_builtin_agent_base_url_commit_is_ignored_like_ts_read_only_input() {
let mut host = WidgetHostNative::new();
host.editor_state_mut()
.editor_ui
.agent_settings
.add_builtin_agent();
host.editor_state_mut().editor_ui.agent_settings.focus = Some(SettingsFocus::BuiltinAgent {
index: 0,
field: BuiltinAgentField::BaseUrl,
});
host.editor_state_mut().editor_ui.settings_input_draft = "https://example.invalid".into();
assert!(host.apply_send());
let settings = &host.editor_state().editor_ui.agent_settings;
assert_eq!(
settings.builtin_agents[0].base_url,
"https://api.anthropic.com"
);
assert!(settings.focus.is_none());
assert!(host
.editor_state()
.editor_ui
.settings_input_draft
.is_empty());
}
#[test]
fn add_provider_opens_unsaved_builtin_agent_draft() {
let mut host = WidgetHostNative::new();

View file

@ -234,6 +234,11 @@ impl WidgetHostNative {
.builtin_agents
.get(index)
{
if field == op_editor_core::agent_settings::BuiltinAgentField::BaseUrl
&& !agent.base_url_editable()
{
return true;
}
self.editor_state.editor_ui.settings_input_draft = match field {
op_editor_core::agent_settings::BuiltinAgentField::DisplayName => {
agent.display_name.clone()

View file

@ -54,11 +54,13 @@ impl WidgetHostNative {
agent.model = draft.trim().to_string();
}
BuiltinAgentField::BaseUrl => {
agent.base_url = if draft.trim().is_empty() {
agent.kind.default_base_url().to_string()
} else {
draft.trim().to_string()
};
if agent.base_url_editable() {
agent.base_url = if draft.trim().is_empty() {
agent.kind.default_base_url().to_string()
} else {
draft.trim().to_string()
};
}
}
}
self.editor_state.rebuild_chat_models();
@ -85,11 +87,13 @@ impl WidgetHostNative {
agent.model = draft.trim().to_string();
}
BuiltinAgentField::BaseUrl => {
agent.base_url = if draft.trim().is_empty() {
agent.kind.default_base_url().to_string()
} else {
draft.trim().to_string()
};
if agent.base_url_editable() {
agent.base_url = if draft.trim().is_empty() {
agent.kind.default_base_url().to_string()
} else {
draft.trim().to_string()
};
}
}
}
}