refactor(editor): centralize agent draft readiness

This commit is contained in:
Kayshen-X 2026-06-14 00:56:30 +08:00
parent ce3626da2d
commit f4c339ed0a
6 changed files with 101 additions and 88 deletions

View file

@ -1346,6 +1346,77 @@ impl EditorUiState {
self.variables_preset_menu_open && self.variables_preset_name_focus
}
pub fn builtin_agent_draft_ready(&self) -> bool {
use crate::agent_settings::BuiltinAgentField;
let Some(name) = self.builtin_agent_draft_field_text(BuiltinAgentField::DisplayName) else {
return false;
};
let Some(api_key) = self.builtin_agent_draft_field_text(BuiltinAgentField::ApiKey) else {
return false;
};
let Some(model) = self.builtin_agent_draft_field_text(BuiltinAgentField::Model) else {
return false;
};
!name.trim().is_empty() && !api_key.trim().is_empty() && !model.trim().is_empty()
}
pub fn acp_agent_draft_ready(&self) -> bool {
use crate::agent_settings::{AcpAgentField, AcpConnectionType};
let Some(draft) = self.agent_settings.acp_agent_draft.as_ref() else {
return false;
};
let Some(name) = self.acp_agent_draft_field_text(AcpAgentField::DisplayName) else {
return false;
};
let endpoint_field = match draft.connection_type {
AcpConnectionType::Local => AcpAgentField::Command,
AcpConnectionType::Remote => AcpAgentField::Url,
};
let Some(endpoint) = self.acp_agent_draft_field_text(endpoint_field) else {
return false;
};
!name.trim().is_empty() && !endpoint.trim().is_empty()
}
pub fn builtin_agent_draft_field_text(
&self,
field: crate::agent_settings::BuiltinAgentField,
) -> Option<&str> {
use crate::agent_settings::{BuiltinAgentField, SettingsFocus};
let draft = self.agent_settings.builtin_agent_draft.as_ref()?;
if self.agent_settings.focus == Some(SettingsFocus::BuiltinAgentDraft(field)) {
return Some(self.settings_input.text());
}
Some(match field {
BuiltinAgentField::DisplayName => draft.display_name.as_str(),
BuiltinAgentField::ApiKey => draft.api_key.as_str(),
BuiltinAgentField::Model => draft.model.as_str(),
BuiltinAgentField::BaseUrl => draft.base_url.as_str(),
})
}
pub fn acp_agent_draft_field_text(
&self,
field: crate::agent_settings::AcpAgentField,
) -> Option<std::borrow::Cow<'_, str>> {
use crate::agent_settings::{AcpAgentField, SettingsFocus};
let draft = self.agent_settings.acp_agent_draft.as_ref()?;
if self.agent_settings.focus == Some(SettingsFocus::AcpAgentDraft(field)) {
return Some(std::borrow::Cow::Borrowed(self.settings_input.text()));
}
Some(match field {
AcpAgentField::DisplayName => std::borrow::Cow::Borrowed(draft.display_name.as_str()),
AcpAgentField::Command => std::borrow::Cow::Borrowed(draft.command.as_str()),
AcpAgentField::Args => std::borrow::Cow::Owned(draft.args_text()),
AcpAgentField::Env => std::borrow::Cow::Owned(draft.env_text()),
AcpAgentField::Url => std::borrow::Cow::Borrowed(draft.url.as_deref().unwrap_or("")),
})
}
/// Clear transient UI state that references specific document nodes/pages
/// or the (now-cleared) selection, so a wholesale document replacement
/// ([`crate::EditorState::replace_document`]) can't leave hover highlights,
@ -1427,6 +1498,34 @@ mod tests {
assert_eq!(ExportFormat::Jpeg.extension(), "jpg");
}
#[test]
fn builtin_agent_draft_ready_reads_focused_settings_input() {
use crate::agent_settings::{BuiltinAgentField, SettingsFocus};
let mut ui = EditorUiState::new();
ui.agent_settings.begin_builtin_agent_draft();
assert!(!ui.builtin_agent_draft_ready());
ui.agent_settings.focus = Some(SettingsFocus::BuiltinAgentDraft(BuiltinAgentField::ApiKey));
ui.settings_input.set_text("sk-test");
assert!(ui.builtin_agent_draft_ready());
}
#[test]
fn acp_agent_draft_ready_reads_focused_settings_input() {
use crate::agent_settings::{AcpAgentField, SettingsFocus};
let mut ui = EditorUiState::new();
ui.agent_settings.begin_acp_agent_draft();
assert!(!ui.acp_agent_draft_ready());
ui.agent_settings.focus = Some(SettingsFocus::AcpAgentDraft(AcpAgentField::Command));
ui.settings_input.set_text("op-agent");
assert!(ui.acp_agent_draft_ready());
}
#[test]
fn dirty_ready_repo_keeps_header_popovers_allowed() {
// TS parity (the ready view now shows for dirty trees too): a

View file

@ -1,7 +1,6 @@
//! ACP Agent section for the Agent settings panel.
use crate::theme::Theme;
use crate::widgets::agent_settings_acp_draft;
use crate::widgets::agent_settings_caret::{paint_settings_input_view, settings_input_text};
use crate::widgets::agent_settings_form_actions::{
cancel_button_rect, paint_form_actions, save_button_rect,
@ -195,7 +194,7 @@ pub fn paint_acp_section(
ui,
card,
EXPANDED_CARD_H,
agent_settings_acp_draft::ready(settings, ui),
ui.acp_agent_draft_ready(),
);
y += card.size.y + CARD_GAP;
}

View file

@ -1,43 +0,0 @@
//! ACP agent draft helpers.
use op_editor_core::agent_settings::{
AcpAgentField, AcpConnectionType, AgentSettings, SettingsFocus,
};
use op_editor_core::editor_ui_state::EditorUiState;
pub fn ready(settings: &AgentSettings, ui: &EditorUiState) -> bool {
let Some(draft) = settings.acp_agent_draft.as_ref() else {
return false;
};
let name = field_value(
settings,
ui,
AcpAgentField::DisplayName,
&draft.display_name,
);
let endpoint = match draft.connection_type {
AcpConnectionType::Local => {
field_value(settings, ui, AcpAgentField::Command, &draft.command)
}
AcpConnectionType::Remote => field_value(
settings,
ui,
AcpAgentField::Url,
draft.url.as_deref().unwrap_or(""),
),
};
!name.trim().is_empty() && !endpoint.trim().is_empty()
}
fn field_value<'a>(
settings: &AgentSettings,
ui: &'a EditorUiState,
field: AcpAgentField,
fallback: &'a str,
) -> &'a str {
if settings.focus == Some(SettingsFocus::AcpAgentDraft(field)) {
ui.settings_input.text()
} else {
fallback
}
}

View file

@ -1,7 +1,6 @@
//! Built-in provider section for the Agent settings panel.
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,
@ -309,14 +308,7 @@ pub fn paint_builtin_section(
);
paint_builtin_agent_form(cx, theme, settings, ui, draft, None, card, now_ms);
let form_h = expanded_card_height(settings, None);
paint_form_actions(
cx,
theme,
ui,
card,
form_h,
agent_settings_builtin_draft::ready(settings, ui),
);
paint_form_actions(cx, theme, ui, card, form_h, ui.builtin_agent_draft_ready());
y += card.size.y + CARD_GAP;
}
y

View file

@ -1,32 +0,0 @@
//! Built-in provider draft helpers.
use op_editor_core::agent_settings::{AgentSettings, BuiltinAgentField, SettingsFocus};
use op_editor_core::editor_ui_state::EditorUiState;
pub fn ready(settings: &AgentSettings, ui: &EditorUiState) -> bool {
let Some(draft) = settings.builtin_agent_draft.as_ref() else {
return false;
};
let api_key = field_value(settings, ui, BuiltinAgentField::ApiKey, &draft.api_key);
let model = field_value(settings, ui, BuiltinAgentField::Model, &draft.model);
let name = field_value(
settings,
ui,
BuiltinAgentField::DisplayName,
&draft.display_name,
);
!api_key.trim().is_empty() && !model.trim().is_empty() && !name.trim().is_empty()
}
fn field_value<'a>(
settings: &AgentSettings,
ui: &'a EditorUiState,
field: BuiltinAgentField,
fallback: &'a str,
) -> &'a str {
if settings.focus == Some(SettingsFocus::BuiltinAgentDraft(field)) {
ui.settings_input.text()
} else {
fallback
}
}

View file

@ -117,9 +117,7 @@ pub mod brand_icons;
// Step 4 — extra editor-chrome widgets (TS app parity).
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)]