fix(settings): track image search test status

This commit is contained in:
Fini 2026-06-01 06:27:36 +08:00
parent 3d1c0b6ae1
commit 0c61ba5b8e
10 changed files with 275 additions and 165 deletions

View file

@ -135,6 +135,14 @@ pub enum ImageSearchField {
ClientSecret,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageTestStatus {
Idle,
Testing,
Valid,
Invalid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AcpAgentField {
DisplayName,
@ -433,6 +441,7 @@ pub struct AgentSettings {
pub mcp_cli_enabled: [bool; 6],
pub images_advanced_open: bool,
pub images_search_ready: bool,
pub images_search_test_status: ImageTestStatus,
pub openverse_client_id: String,
pub openverse_client_secret: String,
pub image_gen_profiles: Vec<ImageGenProfile>,
@ -474,6 +483,7 @@ impl Default for AgentSettings {
mcp_cli_enabled: [false; 6],
images_advanced_open: false,
images_search_ready: true,
images_search_test_status: ImageTestStatus::Idle,
openverse_client_id: String::new(),
openverse_client_secret: String::new(),
image_gen_profiles: Vec::new(),

View file

@ -76,7 +76,8 @@ mod tests_pages;
pub use agent_settings::{
AcpAgentConfig, AcpAgentField, AcpConnectionType, AgentSettings, AgentSettingsDrag,
AgentSettingsTab, BuiltinAgentConfig, BuiltinAgentField, BuiltinAgentKind, ImageGenField,
ImageGenProfile, ImageGenProvider, ImageSearchField, McpCli, McpServer, SettingsFocus,
ImageGenProfile, ImageGenProvider, ImageSearchField, ImageTestStatus, McpCli, McpServer,
SettingsFocus,
};
pub use agent_settings_builtin_presets::{
builtin_agent_preset, infer_builtin_agent_preset, normalize_builtin_agent_preset,

View file

@ -1,6 +1,6 @@
use crate::agent_settings::{
AcpConnectionType, AgentSettings, AgentSettingsTab, BuiltinAgentConfig, BuiltinAgentKind,
ImageGenProvider, McpCli,
ImageGenProvider, ImageTestStatus, McpCli,
};
use crate::agent_settings_builtin_presets::BuiltinAgentPresetKey;
@ -18,6 +18,7 @@ fn default_settings_are_quiescent() {
assert!(!s.images_advanced_open);
assert!(s.openverse_client_id.is_empty());
assert!(s.openverse_client_secret.is_empty());
assert_eq!(s.images_search_test_status, ImageTestStatus::Idle);
assert_eq!(s.mcp_server.port, 3100);
assert!(s.auto_update_enabled);
assert!(s.focus.is_none());

View file

@ -11,7 +11,7 @@ use crate::widgets::PaintCx;
use crate::{Color, Point2D, Rect, TextLayout};
use op_editor_core::agent_settings::{
AgentSettings, ImageGenField, ImageGenProfile, ImageGenProvider, ImageSearchField,
SettingsFocus,
ImageTestStatus, SettingsFocus,
};
use op_editor_core::editor_ui_state::EditorUiState;
@ -131,6 +131,11 @@ fn has_search_credentials(settings: &AgentSettings) -> bool {
|| !settings.openverse_client_secret.trim().is_empty()
}
fn search_test_enabled(settings: &AgentSettings) -> bool {
has_search_credentials(settings)
&& settings.images_search_test_status != ImageTestStatus::Testing
}
fn test_btn_rect(content: Rect, settings: &AgentSettings) -> Rect {
if !settings.images_advanced_open {
return Rect {
@ -273,7 +278,7 @@ pub fn hit_test(content: Rect, settings: &AgentSettings, scrolled: Point2D) -> I
if rect_contains(search_field_rect(content, 1), scrolled) {
return ImagesHit::FocusSearchField(ImageSearchField::ClientSecret);
}
if has_search_credentials(settings)
if search_test_enabled(settings)
&& rect_contains(test_btn_rect(content, settings), scrolled)
{
return ImagesHit::TestSearch;
@ -476,6 +481,7 @@ pub(super) fn paint_images_tab(
1.6,
);
let test_btn = test_btn_rect(content, settings);
paint_search_test_status(cx, theme, settings, test_btn);
cx.backend.fill_round_rect(test_btn, 6.0, theme.muted);
cx.backend
.stroke_round_rect(test_btn, 6.0, theme.border, 1.0);
@ -485,7 +491,11 @@ pub(super) fn paint_images_tab(
test_label,
"system-ui",
13.0,
to_jian(theme.foreground),
to_jian(if search_test_enabled(settings) {
theme.foreground
} else {
theme.muted_foreground
}),
Point2D::new(0.0, 0.0),
);
cx.backend.draw_text(
@ -554,6 +564,46 @@ pub(super) fn paint_images_tab(
}
}
fn paint_search_test_status(
cx: &mut PaintCx<'_>,
theme: &Theme,
settings: &AgentSettings,
test_btn: Rect,
) {
match settings.images_search_test_status {
ImageTestStatus::Idle => {}
ImageTestStatus::Testing => draw_icon(
cx.backend,
Icon::Loader,
Point2D::new(test_btn.origin.x - 20.0, test_btn.origin.y + 8.5),
11.0,
theme.muted_foreground,
1.5,
),
ImageTestStatus::Valid => draw_icon(
cx.backend,
Icon::Check,
Point2D::new(test_btn.origin.x - 20.0, test_btn.origin.y + 8.5),
11.0,
theme.primary,
1.8,
),
ImageTestStatus::Invalid => {
let label = TextLayout::single_run(
"Invalid",
"system-ui",
10.0,
to_jian(theme.destructive),
Point2D::new(0.0, 0.0),
);
cx.backend.draw_text(
&label,
Point2D::new(test_btn.origin.x - 44.0, test_btn.origin.y + 17.0),
);
}
}
}
#[allow(clippy::too_many_arguments)]
fn paint_profile_row(
cx: &mut PaintCx<'_>,

View file

@ -3,7 +3,7 @@ use crate::widgets::{PaintCx, Widget};
use crate::{Color, Point2D, Rect, RenderBackend, TextLayout};
use op_editor_core::agent_settings::{
AcpAgentField, AgentSettingsTab, BuiltinAgentField, ImageGenField, ImageSearchField,
SettingsFocus,
ImageTestStatus, SettingsFocus,
};
use op_editor_core::EditorState;
@ -753,6 +753,26 @@ fn images_tab_test_search_requires_some_oauth_text() {
);
}
#[test]
fn images_tab_test_search_is_disabled_while_testing_like_ts() {
let mut state = EditorState::default();
state.editor_ui.agent_settings.tab = AgentSettingsTab::Images;
state.editor_ui.agent_settings.images_advanced_open = true;
state.editor_ui.agent_settings.openverse_client_id = "client".into();
state.editor_ui.agent_settings.images_search_test_status = ImageTestStatus::Testing;
let panel = AgentSettingsPanel::for_editor(&state);
let rect = panel.rect(1200.0, 800.0);
let content_y = rect.origin.y + 24.0;
let content_w = rect.size.x - 200.0 - 48.0;
let button_x = rect.origin.x + 200.0 + 24.0 + content_w - 28.0;
let button_y = content_y + 36.0 + 24.0 + 22.0 + 36.0 + 10.0 + 36.0 + 14.0 + 18.0;
assert_eq!(
panel.hit_test(rect, crate::Point2D::new(button_x, button_y)),
AgentSettingsHit::Inside
);
}
#[test]
fn images_tab_content_height_includes_profile_rows() {
let mut empty = EditorState::default();

View file

@ -1,7 +1,7 @@
use super::WidgetHostNative;
use op_editor_core::agent_settings::{
AcpAgentField, AgentSettingsTab, BuiltinAgentField, ImageGenField, ImageGenProvider,
ImageSearchField, SettingsFocus,
ImageSearchField, ImageTestStatus, SettingsFocus,
};
use op_editor_core::BuiltinAgentPresetKey;
use op_editor_ui::widgets::agent_settings_panel::AgentSettingsPanel;
@ -743,7 +743,7 @@ fn image_search_oauth_focus_accepts_text_and_commits() {
}
#[test]
fn image_search_test_keeps_search_ready_when_oauth_is_incomplete() {
fn image_search_test_tracks_invalid_and_testing_status_like_ts() {
let mut host = WidgetHostNative::new();
host.editor_state_mut().editor_ui.agent_settings.tab = AgentSettingsTab::Images;
host.editor_state_mut()
@ -763,6 +763,13 @@ fn image_search_test_keeps_search_ready_when_oauth_is_incomplete() {
let y = content_y + 36.0 + 24.0 + 22.0 + 36.0 + 10.0 + 36.0 + 14.0 + 18.0;
assert!(host.dispatch_agent_settings_press(x, y, 1200.0, 800.0));
assert_eq!(
host.editor_state()
.editor_ui
.agent_settings
.images_search_test_status,
ImageTestStatus::Invalid
);
assert!(
host.editor_state()
.editor_ui
@ -775,6 +782,13 @@ fn image_search_test_keeps_search_ready_when_oauth_is_incomplete() {
.agent_settings
.openverse_client_secret = "secret".into();
assert!(host.dispatch_agent_settings_press(x, y, 1200.0, 800.0));
assert_eq!(
host.editor_state()
.editor_ui
.agent_settings
.images_search_test_status,
ImageTestStatus::Testing
);
assert!(
host.editor_state()
.editor_ui

View file

@ -144,7 +144,14 @@ impl WidgetHostNative {
AgentSettingsHit::TestImageSearch => {
self.commit_settings_focus_if_any();
let settings = &mut self.editor_state.editor_ui.agent_settings;
let has_client_id = !settings.openverse_client_id.trim().is_empty();
let has_client_secret = !settings.openverse_client_secret.trim().is_empty();
settings.images_search_ready = true;
settings.images_search_test_status = if has_client_id && has_client_secret {
op_editor_core::agent_settings::ImageTestStatus::Testing
} else {
op_editor_core::agent_settings::ImageTestStatus::Invalid
};
}
AgentSettingsHit::SetActiveGenConfig(index) => {
self.commit_settings_focus_if_any();

View file

@ -44,6 +44,8 @@ use op_editor_ui::widgets::{
use op_editor_ui::{Point2D, Rect, Theme};
mod agent_settings_press;
#[cfg(test)]
mod agent_settings_press_tests;
mod boolean_ops;
#[cfg(test)]
mod boolean_toolbar_tests;

View file

@ -3,7 +3,7 @@
use super::WidgetHost;
use op_editor_core::agent_settings::{
AcpAgentField, AcpConnectionType, AgentProvider, BuiltinAgentField, ImageGenField,
ImageSearchField, McpCli, SettingsFocus,
ImageSearchField, ImageTestStatus, McpCli, SettingsFocus,
};
use op_editor_ui::widgets::agent_settings_panel::{AgentSettingsHit, AgentSettingsPanel};
use op_editor_ui::Point2D;
@ -99,8 +99,14 @@ impl WidgetHost {
AgentSettingsHit::TestImageSearch => {
self.commit_settings_focus();
let settings = &mut self.editor_state.editor_ui.agent_settings;
settings.images_search_ready = !settings.openverse_client_id.trim().is_empty()
&& !settings.openverse_client_secret.trim().is_empty();
let has_client_id = !settings.openverse_client_id.trim().is_empty();
let has_client_secret = !settings.openverse_client_secret.trim().is_empty();
settings.images_search_ready = true;
settings.images_search_test_status = if has_client_id && has_client_secret {
ImageTestStatus::Testing
} else {
ImageTestStatus::Invalid
};
}
AgentSettingsHit::SetActiveGenConfig(index) => {
self.commit_settings_focus();
@ -669,157 +675,3 @@ impl WidgetHost {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use op_editor_core::agent_settings::AgentSettingsTab;
#[test]
fn toggling_builtin_kind_commits_focused_api_key_draft() {
let mut host = WidgetHost::new();
host.editor_state
.editor_ui
.agent_settings
.add_builtin_agent_with_defaults("MINIMAX", "", "MiniMax-M2.7");
host.editor_state.editor_ui.agent_settings.focus = Some(SettingsFocus::BuiltinAgent {
index: 0,
field: BuiltinAgentField::ApiKey,
});
host.editor_state.editor_ui.settings_input_draft = "sk-web".into();
let panel = AgentSettingsPanel::for_editor(&host.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 content_w = rect.size.x - 200.0 - 48.0;
let first_card_y = content_y + 12.0 + 28.0 + 28.0;
let kind_x = content_x + content_w - 172.0 + 120.0;
let kind_y = first_card_y + 22.0;
assert!(host.dispatch_agent_settings_press(kind_x, kind_y, 1200.0, 800.0));
let agent = &host.editor_state.editor_ui.agent_settings.builtin_agents[0];
assert_eq!(agent.api_key, "sk-web");
assert_eq!(
agent.kind,
op_editor_core::agent_settings::BuiltinAgentKind::OpenAiCompat
);
assert!(host.editor_state.editor_ui.agent_settings.focus.is_none());
}
#[test]
fn add_provider_opens_unsaved_builtin_agent_draft() {
let mut host = WidgetHost::new();
host.set_now_ms(1234);
let panel = AgentSettingsPanel::for_editor(&host.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 content_w = rect.size.x - 200.0 - 48.0;
let add_x = content_x + content_w - 48.0;
let add_y = content_y + 24.0;
assert!(host.dispatch_agent_settings_press(add_x, add_y, 1200.0, 800.0));
let settings = &host.editor_state.editor_ui.agent_settings;
assert!(settings.builtin_agents.is_empty());
assert!(settings.builtin_agent_draft.is_some());
assert_eq!(
settings.focus,
Some(SettingsFocus::BuiltinAgentDraft(BuiltinAgentField::ApiKey))
);
assert_eq!(host.editor_state.editor_ui.settings_input_draft, "");
assert_eq!(
host.editor_state.editor_ui.settings_input_caret_anchor_ms,
1234
);
}
#[test]
fn builtin_provider_menu_selects_ts_preset_for_draft() {
let mut host = WidgetHost::new();
let panel = AgentSettingsPanel::for_editor(&host.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 content_w = rect.size.x - 200.0 - 48.0;
let add_x = content_x + content_w - 48.0;
let add_y = content_y + 24.0;
assert!(host.dispatch_agent_settings_press(add_x, add_y, 1200.0, 800.0));
let card_y = content_y + 12.0 + 28.0 + 28.0;
let provider_x = content_x + 68.0 + 24.0;
let provider_y = card_y + 60.0;
assert!(host.dispatch_agent_settings_press(provider_x, provider_y, 1200.0, 800.0));
let minimax_y = card_y + 76.0 + 4.0 + 5.0 * 24.0 + 12.0;
assert!(host.dispatch_agent_settings_press(provider_x, minimax_y, 1200.0, 800.0));
let draft = host
.editor_state
.editor_ui
.agent_settings
.builtin_agent_draft
.as_ref()
.expect("draft remains open");
assert_eq!(draft.preset, op_editor_core::BuiltinAgentPresetKey::MiniMax);
assert_eq!(draft.display_name, "MiniMax");
assert_eq!(draft.model, "MiniMax-M2.7");
assert_eq!(draft.base_url, "https://api.minimaxi.com/anthropic");
}
#[test]
fn save_builtin_agent_draft_persists_provider() {
let mut host = WidgetHost::new();
let panel = AgentSettingsPanel::for_editor(&host.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 content_w = rect.size.x - 200.0 - 48.0;
let add_x = content_x + content_w - 48.0;
let add_y = content_y + 24.0;
assert!(host.dispatch_agent_settings_press(add_x, add_y, 1200.0, 800.0));
for c in "sk-web".chars() {
assert!(host.apply_text(c));
}
let card_y = content_y + 12.0 + 28.0 + 28.0;
let save_x = content_x + content_w - 12.0 - 34.0;
let save_y = card_y + 196.0 + 18.0;
assert!(host.dispatch_agent_settings_press(save_x, save_y, 1200.0, 800.0));
let settings = &host.editor_state.editor_ui.agent_settings;
assert_eq!(settings.builtin_agents.len(), 1);
assert!(settings.builtin_agent_draft.is_none());
assert_eq!(settings.builtin_agents[0].api_key, "sk-web");
assert!(settings.focus.is_none());
}
#[test]
fn image_generation_profile_header_click_toggles_editor_closed() {
let mut host = WidgetHost::new();
host.editor_state.editor_ui.agent_settings.tab = AgentSettingsTab::Images;
host.editor_state
.editor_ui
.agent_settings
.add_image_gen_profile();
host.editor_state.editor_ui.agent_settings.focus = Some(SettingsFocus::ImageGenProfile {
index: 0,
field: ImageGenField::Name,
});
host.editor_state.editor_ui.settings_input_draft = "Config 1".into();
let panel = AgentSettingsPanel::for_editor(&host.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 gen_top = content_y + 36.0 + 24.0 + 28.0;
let row_y = gen_top + 36.0;
assert!(host.dispatch_agent_settings_press(content_x + 72.0, row_y + 16.0, 1200.0, 800.0));
assert_eq!(host.editor_state.editor_ui.agent_settings.focus, None);
assert!(host.editor_state.editor_ui.settings_input_draft.is_empty());
}
}

View file

@ -0,0 +1,153 @@
use super::WidgetHost;
use op_editor_core::agent_settings::{
AgentSettingsTab, BuiltinAgentField, ImageGenField, SettingsFocus,
};
use op_editor_ui::widgets::agent_settings_panel::AgentSettingsPanel;
#[test]
fn toggling_builtin_kind_commits_focused_api_key_draft() {
let mut host = WidgetHost::new();
host.editor_state
.editor_ui
.agent_settings
.add_builtin_agent_with_defaults("MINIMAX", "", "MiniMax-M2.7");
host.editor_state.editor_ui.agent_settings.focus = Some(SettingsFocus::BuiltinAgent {
index: 0,
field: BuiltinAgentField::ApiKey,
});
host.editor_state.editor_ui.settings_input_draft = "sk-web".into();
let panel = AgentSettingsPanel::for_editor(&host.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 content_w = rect.size.x - 200.0 - 48.0;
let first_card_y = content_y + 12.0 + 28.0 + 28.0;
let kind_x = content_x + content_w - 172.0 + 120.0;
let kind_y = first_card_y + 22.0;
assert!(host.dispatch_agent_settings_press(kind_x, kind_y, 1200.0, 800.0));
let agent = &host.editor_state.editor_ui.agent_settings.builtin_agents[0];
assert_eq!(agent.api_key, "sk-web");
assert_eq!(
agent.kind,
op_editor_core::agent_settings::BuiltinAgentKind::OpenAiCompat
);
assert!(host.editor_state.editor_ui.agent_settings.focus.is_none());
}
#[test]
fn add_provider_opens_unsaved_builtin_agent_draft() {
let mut host = WidgetHost::new();
host.set_now_ms(1234);
let panel = AgentSettingsPanel::for_editor(&host.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 content_w = rect.size.x - 200.0 - 48.0;
let add_x = content_x + content_w - 48.0;
let add_y = content_y + 24.0;
assert!(host.dispatch_agent_settings_press(add_x, add_y, 1200.0, 800.0));
let settings = &host.editor_state.editor_ui.agent_settings;
assert!(settings.builtin_agents.is_empty());
assert!(settings.builtin_agent_draft.is_some());
assert_eq!(
settings.focus,
Some(SettingsFocus::BuiltinAgentDraft(BuiltinAgentField::ApiKey))
);
assert_eq!(host.editor_state.editor_ui.settings_input_draft, "");
assert_eq!(
host.editor_state.editor_ui.settings_input_caret_anchor_ms,
1234
);
}
#[test]
fn builtin_provider_menu_selects_ts_preset_for_draft() {
let mut host = WidgetHost::new();
let panel = AgentSettingsPanel::for_editor(&host.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 content_w = rect.size.x - 200.0 - 48.0;
let add_x = content_x + content_w - 48.0;
let add_y = content_y + 24.0;
assert!(host.dispatch_agent_settings_press(add_x, add_y, 1200.0, 800.0));
let card_y = content_y + 12.0 + 28.0 + 28.0;
let provider_x = content_x + 68.0 + 24.0;
let provider_y = card_y + 60.0;
assert!(host.dispatch_agent_settings_press(provider_x, provider_y, 1200.0, 800.0));
let minimax_y = card_y + 76.0 + 4.0 + 5.0 * 24.0 + 12.0;
assert!(host.dispatch_agent_settings_press(provider_x, minimax_y, 1200.0, 800.0));
let draft = host
.editor_state
.editor_ui
.agent_settings
.builtin_agent_draft
.as_ref()
.expect("draft remains open");
assert_eq!(draft.preset, op_editor_core::BuiltinAgentPresetKey::MiniMax);
assert_eq!(draft.display_name, "MiniMax");
assert_eq!(draft.model, "MiniMax-M2.7");
assert_eq!(draft.base_url, "https://api.minimaxi.com/anthropic");
}
#[test]
fn save_builtin_agent_draft_persists_provider() {
let mut host = WidgetHost::new();
let panel = AgentSettingsPanel::for_editor(&host.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 content_w = rect.size.x - 200.0 - 48.0;
let add_x = content_x + content_w - 48.0;
let add_y = content_y + 24.0;
assert!(host.dispatch_agent_settings_press(add_x, add_y, 1200.0, 800.0));
for c in "sk-web".chars() {
assert!(host.apply_text(c));
}
let card_y = content_y + 12.0 + 28.0 + 28.0;
let save_x = content_x + content_w - 12.0 - 34.0;
let save_y = card_y + 196.0 + 18.0;
assert!(host.dispatch_agent_settings_press(save_x, save_y, 1200.0, 800.0));
let settings = &host.editor_state.editor_ui.agent_settings;
assert_eq!(settings.builtin_agents.len(), 1);
assert!(settings.builtin_agent_draft.is_none());
assert_eq!(settings.builtin_agents[0].api_key, "sk-web");
assert!(settings.focus.is_none());
}
#[test]
fn image_generation_profile_header_click_toggles_editor_closed() {
let mut host = WidgetHost::new();
host.editor_state.editor_ui.agent_settings.tab = AgentSettingsTab::Images;
host.editor_state
.editor_ui
.agent_settings
.add_image_gen_profile();
host.editor_state.editor_ui.agent_settings.focus = Some(SettingsFocus::ImageGenProfile {
index: 0,
field: ImageGenField::Name,
});
host.editor_state.editor_ui.settings_input_draft = "Config 1".into();
let panel = AgentSettingsPanel::for_editor(&host.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 gen_top = content_y + 36.0 + 24.0 + 28.0;
let row_y = gen_top + 36.0;
assert!(host.dispatch_agent_settings_press(content_x + 72.0, row_y + 16.0, 1200.0, 800.0));
assert_eq!(host.editor_state.editor_ui.agent_settings.focus, None);
assert!(host.editor_state.editor_ui.settings_input_draft.is_empty());
}