diff --git a/crates/op-editor-ui/src/widgets/agent_settings_embed_tests.rs b/crates/op-editor-ui/src/widgets/agent_settings_embed_tests.rs new file mode 100644 index 000000000..ac5543c73 --- /dev/null +++ b/crates/op-editor-ui/src/widgets/agent_settings_embed_tests.rs @@ -0,0 +1,137 @@ +//! Settings-dialog gating for the VS Code / Cursor custom-editor embed. +//! +//! Product intent (binding): when `ui.embed == EmbedHost::VsCode` the +//! `Cmd+,` dialog exposes ONLY the MCP section (server info + CLI +//! integrations). Built-in API-key agent cards, CLI/ACP agent config, +//! image-provider profiles, and system/experimental sections must +//! neither paint nor hit-test. Outside the embed, behavior is unchanged +//! (covered by every other suite in `agent_settings_panel_tests.rs`). + +use crate::widgets::agent_settings_i18n::t as t_settings; +use crate::widgets::agent_settings_mcp; +use crate::widgets::agent_settings_panel::AgentSettingsPanel; +use crate::widgets::agent_settings_panel_geometry::nav_item_rect; +use crate::widgets::{PaintCx, Widget}; +use crate::{Color, Point2D, Rect, RenderBackend, TextLayout}; +use op_editor_core::agent_settings::AgentSettingsTab; +use op_editor_core::{EditorState, EmbedHost}; + +/// Capture backend recording every text run's content so nav-label +/// paint calls are assertable. +#[derive(Default)] +struct TextCapture { + texts: Vec, +} + +impl RenderBackend for TextCapture { + fn begin_frame(&mut self) {} + fn end_frame(&mut self) {} + fn fill_rect(&mut self, _: Rect, _: Color) {} + fn stroke_rect(&mut self, _: Rect, _: Color, _: f32) {} + fn draw_text(&mut self, layout: &TextLayout, _: Point2D) { + for run in layout.runs() { + self.texts.push(run.content.clone()); + } + } + fn clip_rect(&mut self, _: Rect) {} + fn stroke_line(&mut self, _: Point2D, _: Point2D, _: Color, _: f32) {} + fn fill_round_rect(&mut self, _: Rect, _: f32, _: Color) {} + fn stroke_round_rect(&mut self, _: Rect, _: f32, _: Color, _: f32) {} + fn stroke_svg_path(&mut self, _: &str, _: Point2D, _: f32, _: Color, _: f32) {} + fn save(&mut self) {} + fn restore(&mut self) {} + fn translate(&mut self, _: Point2D) {} + fn resize(&mut self, _: u32, _: u32) {} + fn dpi_scale(&self) -> f32 { + 1.0 + } +} + +fn vscode_embed_state() -> EditorState { + let mut state = EditorState::default(); + // The default locale is ZhCn — pin EN so label comparisons match + // the English table regardless of which table this test runs under. + state.editor_ui.locale = op_i18n::Locale::EnUs; + state.editor_ui.embed = EmbedHost::VsCode; + state +} + +fn nav_row_center(panel_rect: Rect, index: usize) -> Point2D { + let row = nav_item_rect(panel_rect, index); + Point2D::new( + row.origin.x + row.size.x / 2.0, + row.origin.y + row.size.y / 2.0, + ) +} + +#[test] +fn vscode_embed_nav_strip_has_only_the_mcp_row() { + let state = vscode_embed_state(); + let panel = AgentSettingsPanel::for_web_editor(&state); + let rect = panel.rect(1200.0, 800.0); + + assert_eq!( + panel.nav_at(rect, nav_row_center(rect, 0)), + Some(AgentSettingsTab::Mcp), + "the single visible nav row must be MCP" + ); + assert_eq!( + panel.nav_at(rect, nav_row_center(rect, 1)), + None, + "no second nav row (Agents/Images/System/Account) should hit-test in the VS Code embed" + ); +} + +#[test] +fn vscode_embed_opens_directly_on_mcp_even_though_settings_tab_is_agents() { + let state = vscode_embed_state(); + assert_eq!( + state.editor_ui.agent_settings.tab, + AgentSettingsTab::Agents, + "sanity: the persisted/default settings.tab is Agents" + ); + + let panel = AgentSettingsPanel::for_web_editor(&state); + + // `content_total_height` is keyed off the panel's resolved active + // tab; matching it against the MCP tab's own content height proves + // the dialog renders MCP content directly instead of falling back + // to (or ever showing) Agents. + let expected = agent_settings_mcp::content_height(&state.editor_ui.agent_settings); + assert_eq!(panel.content_total_height(), expected); +} + +#[test] +fn vscode_embed_sidebar_paints_only_the_mcp_nav_label() { + let state = vscode_embed_state(); + let panel = AgentSettingsPanel::for_web_editor(&state); + let rect = panel.rect(1200.0, 800.0); + let mut backend = TextCapture::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + + panel.paint(&mut cx, rect); + + let mcp_label = t_settings(&state.editor_ui, "settings.tab.mcp"); + let agents_label = t_settings(&state.editor_ui, "settings.tab.agents"); + let images_label = t_settings(&state.editor_ui, "settings.tab.images"); + let system_label = t_settings(&state.editor_ui, "settings.tab.system"); + + assert!( + backend.texts.iter().any(|s| s.as_str() == mcp_label), + "MCP nav label should paint" + ); + assert!( + !backend.texts.iter().any(|s| s.as_str() == agents_label), + "Agents nav label must not paint in the VS Code embed" + ); + assert!( + !backend.texts.iter().any(|s| s.as_str() == images_label), + "Images nav label must not paint in the VS Code embed" + ); + assert!( + !backend.texts.iter().any(|s| s.as_str() == system_label), + "System nav label must not paint in the VS Code embed" + ); +} diff --git a/crates/op-editor-ui/src/widgets/agent_settings_panel.rs b/crates/op-editor-ui/src/widgets/agent_settings_panel.rs index 985ec0c94..e1c0eb02f 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_panel.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_panel.rs @@ -49,9 +49,19 @@ pub(super) const SUB_FONT: f32 = 11.0; pub enum AgentSettingsPanelMode { Full, WebBuiltinOnly, + /// VS Code / Cursor custom-editor embed (`ui.embed == EmbedHost::VsCode`): + /// the dialog exposes only the MCP section (server info + CLI + /// integrations) — no built-in agent cards, CLI/ACP config, image + /// providers, or system/experimental settings paint or hit-test. + McpOnly, } impl AgentSettingsPanelMode { + /// Single enumeration point for the dialog's sections: both the + /// nav-strip paint (`paint_sidebar`) and every hit-test entry + /// point (`hit_test`, `nav_at`, `card_at`, `acp_card_at`, + /// `content_total_height`) walk this same slice, so gating here + /// keeps paint and hit-test in lockstep automatically. fn visible_tabs(self) -> &'static [AgentSettingsTab] { match self { AgentSettingsPanelMode::Full => &AgentSettingsTab::ALL, @@ -60,6 +70,7 @@ impl AgentSettingsPanelMode { AgentSettingsTab::Images, AgentSettingsTab::System, ], + AgentSettingsPanelMode::McpOnly => &[AgentSettingsTab::Mcp], } } @@ -67,7 +78,10 @@ impl AgentSettingsPanelMode { if self.visible_tabs().contains(&settings.tab) { settings.tab } else { - AgentSettingsTab::Agents + // Fall back to this mode's first visible tab rather than a + // hardcoded `Agents` — `McpOnly` never lists `Agents`, so the + // dialog must land on MCP directly when opened in embed. + self.visible_tabs()[0] } } @@ -76,6 +90,18 @@ impl AgentSettingsPanelMode { } } +/// Narrow `base` down to `McpOnly` when the editor is rendering inside the +/// VS Code / Cursor embed, regardless of which panel flavor the host would +/// otherwise request. Reads `ui.embed` — the same source `top_bar.rs` uses +/// to relabel the settings chip to "MCP" for this embed. +fn mode_for_ui(ui: &EditorUiState, base: AgentSettingsPanelMode) -> AgentSettingsPanelMode { + if ui.embed == op_editor_core::EmbedHost::VsCode { + AgentSettingsPanelMode::McpOnly + } else { + base + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AgentSettingsHit { Close, @@ -166,7 +192,7 @@ impl<'a> AgentSettingsPanel<'a> { theme: theme_for(&state.editor_ui), settings: state.editor_ui.agent_settings.clone(), now_ms, - mode: AgentSettingsPanelMode::Full, + mode: mode_for_ui(&state.editor_ui, AgentSettingsPanelMode::Full), ui: &state.editor_ui, } } @@ -181,7 +207,7 @@ impl<'a> AgentSettingsPanel<'a> { theme: theme_for(&state.editor_ui), settings: state.editor_ui.agent_settings.clone(), now_ms, - mode: AgentSettingsPanelMode::WebBuiltinOnly, + mode: mode_for_ui(&state.editor_ui, AgentSettingsPanelMode::WebBuiltinOnly), ui: &state.editor_ui, } } diff --git a/crates/op-editor-ui/src/widgets/mod.rs b/crates/op-editor-ui/src/widgets/mod.rs index 3ef772103..72b819d0a 100644 --- a/crates/op-editor-ui/src/widgets/mod.rs +++ b/crates/op-editor-ui/src/widgets/mod.rs @@ -179,6 +179,8 @@ mod agent_settings_compact_action_tests; #[cfg(test)] mod agent_settings_connect_tests; #[cfg(test)] +mod agent_settings_embed_tests; +#[cfg(test)] mod agent_settings_form_action_tests; mod agent_settings_form_actions; mod agent_settings_header_action;