diff --git a/crates/op-editor-core/src/agent_settings.rs b/crates/op-editor-core/src/agent_settings.rs index 288e0aa1a..d179dd2d4 100644 --- a/crates/op-editor-core/src/agent_settings.rs +++ b/crates/op-editor-core/src/agent_settings.rs @@ -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() } diff --git a/crates/op-editor-core/src/tests_agent_settings.rs b/crates/op-editor-core/src/tests_agent_settings.rs index 50c7d7f9a..76071a24d 100644 --- a/crates/op-editor-core/src/tests_agent_settings.rs +++ b/crates/op-editor-core/src/tests_agent_settings.rs @@ -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(); diff --git a/crates/op-editor-ui/src/widgets/agent_settings_builtin.rs b/crates/op-editor-ui/src/widgets/agent_settings_builtin.rs index 41e730a1d..6c3151055 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_builtin.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_builtin.rs @@ -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) -> 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, - 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 diff --git a/crates/op-editor-ui/src/widgets/agent_settings_builtin_layout.rs b/crates/op-editor-ui/src/widgets/agent_settings_builtin_layout.rs new file mode 100644 index 000000000..603ac6614 --- /dev/null +++ b/crates/op-editor-ui/src/widgets/agent_settings_builtin_layout.rs @@ -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) -> 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, + 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 +} diff --git a/crates/op-editor-ui/src/widgets/agent_settings_builtin_tests.rs b/crates/op-editor-ui/src/widgets/agent_settings_builtin_tests.rs new file mode 100644 index 000000000..f70e811b0 --- /dev/null +++ b/crates/op-editor-ui/src/widgets/agent_settings_builtin_tests.rs @@ -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); +} diff --git a/crates/op-editor-ui/src/widgets/mod.rs b/crates/op-editor-ui/src/widgets/mod.rs index 043ba1ea3..4f117c0c2 100644 --- a/crates/op-editor-ui/src/widgets/mod.rs +++ b/crates/op-editor-ui/src/widgets/mod.rs @@ -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; diff --git a/crates/op-host-native/src/widget_host/agent_settings_draft_dispatch.rs b/crates/op-host-native/src/widget_host/agent_settings_draft_dispatch.rs index d21e7074f..5887c204d 100644 --- a/crates/op-host-native/src/widget_host/agent_settings_draft_dispatch.rs +++ b/crates/op-host-native/src/widget_host/agent_settings_draft_dispatch.rs @@ -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(), diff --git a/crates/op-host-native/src/widget_host/agent_settings_tests.rs b/crates/op-host-native/src/widget_host/agent_settings_tests.rs index 51aa77367..eb7a52f29 100644 --- a/crates/op-host-native/src/widget_host/agent_settings_tests.rs +++ b/crates/op-host-native/src/widget_host/agent_settings_tests.rs @@ -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(); diff --git a/crates/op-host-native/src/widget_host/press_helpers.rs b/crates/op-host-native/src/widget_host/press_helpers.rs index 9a23ddcb5..c8f68d7a8 100644 --- a/crates/op-host-native/src/widget_host/press_helpers.rs +++ b/crates/op-host-native/src/widget_host/press_helpers.rs @@ -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() diff --git a/crates/op-host-native/src/widget_host/settings_dispatch.rs b/crates/op-host-native/src/widget_host/settings_dispatch.rs index 9197029e9..78bf5e45d 100644 --- a/crates/op-host-native/src/widget_host/settings_dispatch.rs +++ b/crates/op-host-native/src/widget_host/settings_dispatch.rs @@ -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() + }; + } } } }