use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ConnectProviderStage {
ChooseProvider,
ConfigureCustom,
AnthropicGuidance,
OAuth,
ReplacementConfirmation,
Saving,
Success,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ConnectProviderField {
Label,
BaseUrl,
ApiKeyEnvVar,
ModelsDevProvider,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum OAuthControl {
#[default]
Fallback,
OpenBrowser,
CopyUrl,
}
fn debug_safe_text(value: &str) -> String {
crate::output::redact_sensitive_text(value)
}
fn debug_safe_url(value: &str) -> String {
let value = debug_safe_text(value);
let Some((base, _query)) = value.split_once('?') else {
return value;
};
format!("{base}?<redacted>")
}
fn debug_safe_env_var(value: &str) -> &str {
if value.is_empty() {
"<empty>"
} else if crate::config::looks_like_secret_value(value) {
"<redacted>"
} else {
value
}
}
fn debug_safe_entries(entries: &[LoginProviderEntry]) -> Vec<(String, String, String, String)> {
entries
.iter()
.map(|entry| {
(
debug_safe_text(&entry.id),
debug_safe_text(&entry.label),
debug_safe_text(&entry.status),
debug_safe_text(&entry.description),
)
})
.collect()
}
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct ConnectProviderReplacementState {
pub(crate) provider_id: String,
pub(crate) existing_label: String,
pub(crate) label: String,
pub(crate) base_url: String,
pub(crate) api_key_env_var: Option<String>,
pub(crate) models_dev_provider: String,
}
impl std::fmt::Debug for ConnectProviderReplacementState {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let api_key_env_var = self.api_key_env_var.as_deref().map(debug_safe_env_var);
formatter
.debug_struct("ConnectProviderReplacementState")
.field("provider_id", &debug_safe_text(&self.provider_id))
.field("existing_label", &debug_safe_text(&self.existing_label))
.field("label", &debug_safe_text(&self.label))
.field("base_url", &debug_safe_url(&self.base_url))
.field("api_key_env_var", &api_key_env_var)
.field(
"models_dev_provider",
&debug_safe_text(&self.models_dev_provider),
)
.finish()
}
}
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct ConnectProviderState {
pub(crate) stage: ConnectProviderStage,
pub(crate) entries: Vec<LoginProviderEntry>,
pub(crate) selected: usize,
pub(crate) selected_field: ConnectProviderField,
pub(crate) provider_id: Option<String>,
pub(crate) label: SingleLineField,
pub(crate) base_url: SingleLineField,
pub(crate) api_key_env_var: SingleLineField,
pub(crate) models_dev_provider: SingleLineField,
pub(crate) oauth_focus: OAuthControl,
pub(crate) oauth_url_page: i32,
pub(crate) instructions: String,
pub(crate) url: String,
pub(crate) fallback_input: SingleLineField,
pub(crate) notice: Option<String>,
pub(crate) error: Option<String>,
pub(crate) success: Option<String>,
pub(crate) retry_stage: Option<ConnectProviderStage>,
pub(crate) replacement: Option<ConnectProviderReplacementState>,
pub(crate) runtime_refresh_pending: bool,
}
impl std::fmt::Debug for ConnectProviderState {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let entries = debug_safe_entries(&self.entries);
let provider_id = self.provider_id.as_deref().map(debug_safe_text);
let label = debug_safe_text(self.label.value());
let base_url = debug_safe_url(self.base_url.value());
let api_key_env_var = debug_safe_env_var(self.api_key_env_var.value());
let notice = self.notice.as_deref().map(debug_safe_text);
let error = self.error.as_deref().map(debug_safe_text);
let success = self.success.as_deref().map(debug_safe_text);
formatter
.debug_struct("ConnectProviderState")
.field("stage", &self.stage)
.field("entries", &entries)
.field("selected", &self.selected)
.field("selected_field", &self.selected_field)
.field("provider_id", &provider_id)
.field("label", &label)
.field("base_url", &base_url)
.field("api_key_env_var", &api_key_env_var)
.field(
"models_dev_provider",
&debug_safe_text(self.models_dev_provider.value()),
)
.field("oauth_focus", &self.oauth_focus)
.field("instructions", &"<redacted>")
.field("url", &"<redacted>")
.field("fallback_input", &"<redacted>")
.field("notice", ¬ice)
.field("error", &error)
.field("success", &success)
.field("retry_stage", &self.retry_stage)
.field("replacement", &self.replacement)
.field("runtime_refresh_pending", &self.runtime_refresh_pending)
.finish()
}
}
impl ConnectProviderState {
pub(super) fn new(entries: Vec<LoginProviderEntry>, notice: Option<String>) -> Self {
Self {
stage: ConnectProviderStage::ChooseProvider,
entries,
selected: 0,
selected_field: ConnectProviderField::Label,
provider_id: None,
label: SingleLineField::plain(),
base_url: SingleLineField::plain(),
api_key_env_var: SingleLineField::plain(),
models_dev_provider: SingleLineField::plain(),
oauth_focus: OAuthControl::Fallback,
oauth_url_page: 0,
instructions: String::new(),
url: String::new(),
fallback_input: SingleLineField::redacted(),
notice,
error: None,
success: None,
retry_stage: None,
replacement: None,
runtime_refresh_pending: false,
}
}
pub(super) fn clamp(&mut self) {
let item_count = self.entries.len();
clamp_picker_selection(&mut self.selected, item_count);
}
pub(super) fn selected_provider_id(&self) -> Option<String> {
self.entries
.get(self.selected)
.map(|entry| entry.id.clone())
}
pub(super) fn current_field_mut(&mut self) -> Option<&mut SingleLineField> {
match self.selected_field {
ConnectProviderField::Label => Some(&mut self.label),
ConnectProviderField::BaseUrl => Some(&mut self.base_url),
ConnectProviderField::ApiKeyEnvVar => Some(&mut self.api_key_env_var),
ConnectProviderField::ModelsDevProvider => Some(&mut self.models_dev_provider),
}
}
pub(super) fn next_field(&mut self) {
self.selected_field = match self.selected_field {
ConnectProviderField::Label => ConnectProviderField::BaseUrl,
ConnectProviderField::BaseUrl => ConnectProviderField::ApiKeyEnvVar,
ConnectProviderField::ApiKeyEnvVar => ConnectProviderField::ModelsDevProvider,
ConnectProviderField::ModelsDevProvider => ConnectProviderField::Label,
};
}
pub(super) fn previous_field(&mut self) {
self.selected_field = match self.selected_field {
ConnectProviderField::Label => ConnectProviderField::ModelsDevProvider,
ConnectProviderField::BaseUrl => ConnectProviderField::Label,
ConnectProviderField::ApiKeyEnvVar => ConnectProviderField::BaseUrl,
ConnectProviderField::ModelsDevProvider => ConnectProviderField::ApiKeyEnvVar,
};
}
}
impl MissionControlState {
pub(crate) fn open_connect_provider(
&mut self,
entries: Vec<LoginProviderEntry>,
notice: Option<String>,
) {
let mut connect = ConnectProviderState::new(entries, notice);
connect.clamp();
self.modals.connect_provider = Some(connect);
self.set_scroll_offset(&self.scroll_views.connect_provider, 0);
self.hide_autocomplete();
}
pub(crate) fn close_connect_provider(&mut self) -> bool {
self.modals.connect_provider.take().is_some()
}
pub(crate) fn connect_provider_up(&mut self, visible_rows: usize) -> bool {
self.move_connect_provider_selection(-1, visible_rows)
}
pub(crate) fn connect_provider_down(&mut self, visible_rows: usize) -> bool {
self.move_connect_provider_selection(1, visible_rows)
}
pub(super) fn move_connect_provider_selection(
&mut self,
amount: i32,
visible_rows: usize,
) -> bool {
let (before_selected, selected) = {
let Some(connect) = &mut self.modals.connect_provider else {
return false;
};
if connect.stage != ConnectProviderStage::ChooseProvider {
return false;
}
let before_selected = connect.selected;
if amount.is_negative() {
connect.selected = connect
.selected
.saturating_sub(amount.unsigned_abs() as usize);
} else {
connect.selected = connect.selected.saturating_add(amount as usize);
}
connect.clamp();
(before_selected, connect.selected)
};
let before_offset = self.scroll_offset(&self.scroll_views.connect_provider);
self.clamp_connect_provider_scroll(visible_rows);
before_selected != selected
|| before_offset != self.scroll_offset(&self.scroll_views.connect_provider)
}
pub(super) fn clamp_connect_provider_scroll(&self, visible_rows: usize) {
let Some(connect) = &self.modals.connect_provider else {
return;
};
let visible_rows = visible_rows.max(1);
let max_offset = connect.entries.len().saturating_sub(visible_rows);
let mut offset = self
.scroll_offset(&self.scroll_views.connect_provider)
.min(max_offset);
if connect.selected < offset {
offset = connect.selected;
} else if connect.selected >= offset.saturating_add(visible_rows) {
offset = connect.selected + 1 - visible_rows;
}
self.set_scroll_offset(&self.scroll_views.connect_provider, offset.min(max_offset));
}
pub(crate) fn selected_connect_provider_id(&self) -> Option<String> {
self.modals
.connect_provider
.as_ref()
.and_then(ConnectProviderState::selected_provider_id)
}
pub(crate) fn start_connect_custom_configuration(&mut self) {
let Some(connect) = &mut self.modals.connect_provider else {
return;
};
connect.stage = ConnectProviderStage::ConfigureCustom;
connect.provider_id = None;
connect.selected_field = ConnectProviderField::Label;
connect.label.clear();
connect.base_url.clear();
connect.api_key_env_var.clear();
connect.models_dev_provider.clear();
connect.instructions.clear();
connect.url.clear();
connect.fallback_input.clear();
connect.notice = None;
connect.error = None;
connect.success = None;
connect.retry_stage = None;
connect.replacement = None;
connect.runtime_refresh_pending = false;
}
pub(crate) fn start_connect_cli_guidance(&mut self, provider_id: &str, instructions: &str) {
if let Some(connect) = &mut self.modals.connect_provider {
connect.stage = ConnectProviderStage::AnthropicGuidance;
connect.provider_id = Some(provider_id.to_string());
connect.instructions = instructions.to_string();
connect.error = None;
connect.success = None;
connect.retry_stage = Some(ConnectProviderStage::AnthropicGuidance);
}
}
pub(crate) fn start_connect_anthropic_guidance(&mut self) {
self.start_connect_cli_guidance(
crate::providers::ANTHROPIC_PROVIDER,
crate::auth::login::anthropic_api_key_setup_guidance(),
);
}
pub(crate) fn start_connect_oauth(&mut self, provider_id: String) {
if let Some(connect) = &mut self.modals.connect_provider {
connect.stage = ConnectProviderStage::OAuth;
connect.provider_id = Some(provider_id);
connect.instructions = "Starting login…".to_string();
connect.url.clear();
connect.fallback_input.clear();
connect.oauth_focus = OAuthControl::Fallback;
connect.oauth_url_page = 0;
connect.error = None;
connect.success = None;
connect.retry_stage = Some(ConnectProviderStage::OAuth);
connect.runtime_refresh_pending = false;
}
}
pub(crate) fn connect_provider_next_field(&mut self) {
if let Some(connect) = &mut self.modals.connect_provider {
match connect.stage {
ConnectProviderStage::ConfigureCustom => {
connect.next_field();
connect.error = None;
}
ConnectProviderStage::OAuth => {
connect.oauth_focus = match connect.oauth_focus {
OAuthControl::Fallback => OAuthControl::OpenBrowser,
OAuthControl::OpenBrowser => OAuthControl::CopyUrl,
OAuthControl::CopyUrl => OAuthControl::Fallback,
}
}
_ => {}
}
}
}
pub(crate) fn connect_provider_previous_field(&mut self) {
if let Some(connect) = &mut self.modals.connect_provider {
match connect.stage {
ConnectProviderStage::ConfigureCustom => {
connect.previous_field();
connect.error = None;
}
ConnectProviderStage::OAuth => {
connect.oauth_focus = match connect.oauth_focus {
OAuthControl::Fallback => OAuthControl::CopyUrl,
OAuthControl::OpenBrowser => OAuthControl::Fallback,
OAuthControl::CopyUrl => OAuthControl::OpenBrowser,
}
}
_ => {}
}
}
}
pub(crate) fn set_connect_provider_instructions(
&mut self,
url: String,
instructions: String,
) -> bool {
let Some(connect) = &mut self.modals.connect_provider else {
return false;
};
if connect.stage != ConnectProviderStage::OAuth {
return false;
}
connect.url = url;
connect.instructions = instructions;
connect.fallback_input.clear();
true
}
pub(crate) fn set_connect_provider_instructions_for(
&mut self,
worker_id: u64,
url: String,
instructions: String,
) -> bool {
if self.active_worker_id != Some(worker_id) {
return false;
}
self.set_connect_provider_instructions(url, instructions)
}
pub(crate) fn open_connect_provider_replacement_confirmation(
&mut self,
provider_id: String,
existing_label: String,
label: String,
base_url: String,
api_key_env_var: Option<String>,
) {
if let Some(connect) = &mut self.modals.connect_provider {
connect.stage = ConnectProviderStage::ReplacementConfirmation;
connect.provider_id = Some(provider_id.clone());
connect.replacement = Some(ConnectProviderReplacementState {
models_dev_provider: connect.models_dev_provider.value().trim().to_string(),
provider_id,
existing_label,
label,
base_url,
api_key_env_var,
});
connect.error = None;
}
}
pub(crate) fn set_connect_provider_saving(&mut self, provider_id: String) {
if let Some(connect) = &mut self.modals.connect_provider {
connect.stage = ConnectProviderStage::Saving;
connect.provider_id = Some(provider_id);
connect.replacement = None;
connect.error = None;
connect.success = None;
connect.retry_stage = Some(ConnectProviderStage::ConfigureCustom);
connect.runtime_refresh_pending = false;
}
}
pub(crate) fn set_connect_provider_success(
&mut self,
provider_id: &str,
message: String,
runtime_refresh_pending: bool,
) -> bool {
let Some(connect) = &mut self.modals.connect_provider else {
return false;
};
if connect.provider_id.as_deref() != Some(provider_id)
|| !matches!(
connect.stage,
ConnectProviderStage::OAuth | ConnectProviderStage::Saving
)
{
return false;
}
connect.stage = ConnectProviderStage::Success;
connect.success = Some(crate::output::redact_sensitive_text(&message));
connect.error = None;
connect.retry_stage = None;
connect.runtime_refresh_pending = runtime_refresh_pending;
connect.fallback_input.clear();
true
}
pub(crate) fn set_connect_provider_error(&mut self, error: impl Into<String>) -> bool {
let Some(connect) = &mut self.modals.connect_provider else {
return false;
};
if connect.stage == ConnectProviderStage::ChooseProvider {
connect.retry_stage = Some(ConnectProviderStage::ChooseProvider);
} else if connect.retry_stage.is_none() {
connect.retry_stage = Some(match connect.stage {
ConnectProviderStage::Saving => ConnectProviderStage::ConfigureCustom,
ConnectProviderStage::OAuth => ConnectProviderStage::OAuth,
ConnectProviderStage::AnthropicGuidance => ConnectProviderStage::AnthropicGuidance,
_ => ConnectProviderStage::ChooseProvider,
});
}
connect.stage = ConnectProviderStage::Error;
connect.error = Some(crate::output::redact_sensitive_text(&error.into()));
connect.success = None;
connect.fallback_input.clear();
connect.runtime_refresh_pending = false;
true
}
pub(crate) fn take_connect_provider_runtime_refresh(&mut self, provider_id: &str) -> bool {
let Some(connect) = &mut self.modals.connect_provider else {
return false;
};
if connect.stage == ConnectProviderStage::Success
&& connect.provider_id.as_deref() == Some(provider_id)
&& connect.runtime_refresh_pending
{
connect.runtime_refresh_pending = false;
true
} else {
false
}
}
pub(crate) fn take_connect_provider_fallback_input(&mut self) -> Option<String> {
self.modals.connect_provider.as_mut()?.fallback_input.take()
}
pub(crate) fn connect_provider_stage(&self) -> Option<ConnectProviderStage> {
self.modals
.connect_provider
.as_ref()
.map(|connect| connect.stage)
}
pub(crate) fn set_connect_provider_form_error(&mut self, error: impl Into<String>) -> bool {
let Some(connect) = &mut self.modals.connect_provider else {
return false;
};
if connect.stage != ConnectProviderStage::ConfigureCustom {
return false;
}
connect.error = Some(crate::output::redact_sensitive_text(&error.into()));
true
}
pub(crate) fn prepare_connect_provider_retry(&mut self) -> Option<ConnectProviderStage> {
let connect = self.modals.connect_provider.as_mut()?;
let stage = connect.retry_stage?;
connect.stage = stage;
connect.error = None;
connect.success = None;
connect.replacement = None;
connect.fallback_input.clear();
Some(stage)
}
pub(crate) fn back_connect_provider(&mut self) -> bool {
let Some(connect) = &mut self.modals.connect_provider else {
return false;
};
let target = match connect.stage {
ConnectProviderStage::ChooseProvider => ConnectProviderStage::ChooseProvider,
ConnectProviderStage::ConfigureCustom => ConnectProviderStage::ChooseProvider,
ConnectProviderStage::AnthropicGuidance => ConnectProviderStage::ChooseProvider,
ConnectProviderStage::OAuth => ConnectProviderStage::ChooseProvider,
ConnectProviderStage::ReplacementConfirmation => ConnectProviderStage::ConfigureCustom,
ConnectProviderStage::Saving => ConnectProviderStage::ConfigureCustom,
ConnectProviderStage::Success => ConnectProviderStage::ChooseProvider,
ConnectProviderStage::Error => match connect.retry_stage {
Some(ConnectProviderStage::ConfigureCustom)
| Some(ConnectProviderStage::ReplacementConfirmation)
| Some(ConnectProviderStage::Saving) => ConnectProviderStage::ConfigureCustom,
_ => ConnectProviderStage::ChooseProvider,
},
};
connect.stage = target;
connect.error = None;
connect.success = None;
connect.replacement = None;
connect.fallback_input.clear();
if target == ConnectProviderStage::ChooseProvider {
connect.provider_id = None;
}
true
}
}