use super::super::*;
use super::MissionControlApp;
impl MissionControlApp {
pub(crate) fn open_connect_provider(&mut self, ui_state: &mut state::MissionControlState) {
let mut auth_error = None;
let entries = crate::login::providers()
.into_iter()
.map(|provider| {
let status = match crate::login::login_provider_status_label(
&self.config.paths,
provider.id,
) {
Ok(status) => status.to_string(),
Err(error) => {
auth_error.get_or_insert_with(|| error.to_string());
"auth store error".to_string()
}
};
state::LoginProviderEntry {
id: provider.id.to_string(),
label: provider.label.to_string(),
description: provider.description.to_string(),
status,
}
})
.collect();
ui_state.open_connect_provider(entries, None);
ui_state.status = auth_error
.map(|error| format!("login auth store error: {error}"))
.unwrap_or_else(|| "select login provider".to_string());
}
pub(crate) fn open_logout_picker(
&mut self,
ui_state: &mut state::MissionControlState,
terminal_area: ratatui::layout::Rect,
) {
let entries_result: anyhow::Result<Vec<_>> = crate::login::providers()
.into_iter()
.map(|provider| {
Ok(state::LoginProviderEntry {
id: provider.id.to_string(),
label: provider.label.to_string(),
description: provider.description.to_string(),
status: crate::login::logout_provider_status(&self.config.paths, provider.id)?
.label()
.to_string(),
})
})
.collect();
match entries_result {
Ok(mut entries) => {
if let Ok(settings) = crate::config::read_settings(&self.config.paths) {
entries.extend(settings.custom_providers.into_iter().map(|(id, provider)| {
state::LoginProviderEntry {
id,
label: provider.label,
description: "custom provider metadata".to_string(),
status: "configured".to_string(),
}
}));
}
ui_state.open_logout_picker(
entries,
None,
logout_picker_visible_rows(terminal_area, false),
);
ui_state.status = "select logout provider".to_string();
}
Err(error) => {
Self::apply_ui_error(ui_state, error);
}
}
}
pub(crate) fn logout_provider(
&mut self,
provider_id: &str,
ui_state: &mut state::MissionControlState,
) {
if self.active_run || self.worker.is_some() {
Self::apply_ui_error(ui_state, "cannot logout while a prompt is running");
return;
}
if let Ok(settings) = crate::config::read_settings(&self.config.paths)
&& let Some(provider) = settings.custom_providers.get(provider_id)
{
ui_state.open_logout_confirmation(provider_id.to_string(), provider.label.clone());
ui_state.status = format!("confirm removal for custom provider {provider_id}");
return;
}
let provider = match crate::login::validate_logout_provider(provider_id) {
Ok(provider) => provider,
Err(error) => {
Self::apply_ui_error(ui_state, error);
return;
}
};
match crate::login::logout_provider_status(&self.config.paths, provider.id) {
Ok(crate::login::LogoutProviderStatus::Missing) => {
ui_state.status = format!(
"{} ({}) is not configured; credentials unchanged",
provider.label, provider.id
);
}
Ok(_) => {
ui_state
.open_logout_confirmation(provider.id.to_string(), provider.label.to_string());
ui_state.status =
format!("confirm logout for {} ({})", provider.label, provider.id);
}
Err(error) => {
Self::apply_ui_error(ui_state, error);
}
}
}
pub(crate) fn remove_logout_provider(
&mut self,
provider_id: &str,
ui_state: &mut state::MissionControlState,
) {
match crate::commands::runtime::remove_logout_target(&self.config.paths, provider_id) {
Ok(removal) => {
if removal.removed() {
crate::commands::runtime::reconcile_runtime_after_logout(
&mut self.state,
Some(&mut self.config),
&removal,
);
ui_state.provider_ready = self.state.auth_state.is_ready();
}
ui_state.status = removal.message();
}
Err(error) => {
Self::apply_ui_error(ui_state, error);
}
}
}
pub(crate) fn start_connect_provider(
&mut self,
provider_id: &str,
ui_state: &mut state::MissionControlState,
) {
if self.active_run || self.worker.is_some() {
Self::apply_ui_error(ui_state, "cannot start login while a prompt is running");
return;
}
if ui_state.modals.connect_provider.is_none() {
self.open_connect_provider(ui_state);
}
match provider_id {
crate::login::CUSTOM_PROVIDER_LOGIN_ID | "custom" => {
ui_state.start_connect_custom_configuration();
ui_state.status = "custom provider setup: enter provider label".to_string();
}
crate::providers::ANTHROPIC_PROVIDER => {
ui_state.start_connect_anthropic_guidance();
ui_state.status = crate::login::anthropic_api_key_setup_guidance().to_string();
}
crate::providers::OPENAI_CODEX_PROVIDER => {
ui_state.start_connect_oauth(provider_id.to_string());
self.start_openai_codex_login_worker(ui_state);
}
_ => {
let message = crate::login::unsupported_login_provider_message(provider_id);
let _ = ui_state.set_connect_provider_error(message.clone());
ui_state.status = message;
}
}
}
fn start_openai_codex_login_worker(&mut self, ui_state: &mut state::MissionControlState) {
let paths = self.config.paths.clone();
let sender = self.events.clone();
let cancel = Arc::new(AtomicBool::new(false));
let outcome = Arc::new(WorkerOutcomeState::default());
let worker_id = outcome.worker_id();
ui_state.active_worker_id = Some(worker_id);
ui_state.last_run_finished_worker_id = None;
self.active_run = true;
let worker_cancel = Arc::clone(&cancel);
let worker_outcome = Arc::clone(&outcome);
let (manual_tx, manual_rx) = mpsc::channel();
let progress_sender = sender.clone();
let handle = thread::spawn(move || {
if worker_cancel.load(Ordering::SeqCst) {
send_completion(&sender, &worker_outcome, None);
return;
}
let result = crate::login::login_openai_codex_with_controls(
&paths,
Arc::clone(&worker_cancel),
Some(manual_rx),
move |instructions| {
let _ = send_critical(
&progress_sender,
TuiEvent::OAuthInstructions {
worker_id,
instructions,
},
);
},
)
.map(|result| result.message)
.map_err(|error| error.to_string());
send_completion(
&sender,
&worker_outcome,
Some(WorkerFinalEvent::OAuthFinished {
provider_id: crate::providers::OPENAI_CODEX_PROVIDER.to_string(),
result,
}),
);
});
self.worker = Some(WorkerState {
handle,
cancel,
login_manual: Some(manual_tx),
outcome,
outcome_reconciled: false,
shutdown_policy: WorkerShutdownPolicy::Cancel,
steering: crate::agent::steering::AgentSteering::new(),
accepts_steering: false,
});
}
pub(crate) fn submit_connect_provider_setup_step(
&mut self,
ui_state: &mut state::MissionControlState,
) {
let Some(connect) = ui_state.modals.connect_provider.clone() else {
ui_state.status = "no provider connection is active".to_string();
return;
};
if connect.stage != state::ConnectProviderStage::ConfigureCustom {
ui_state.status = "custom provider form is not active".to_string();
return;
}
let label = connect.label.value().trim().to_string();
let provider_id = match crate::config::derive_custom_provider_id(&label) {
Ok(provider_id) => provider_id,
Err(error) => {
let message = error.to_string();
ui_state.set_connect_provider_form_error(message.clone());
ui_state.status = message;
return;
}
};
let base_url =
match crate::config::normalize_custom_provider_base_url(connect.base_url.value()) {
Ok(base_url) => base_url,
Err(error) => {
let message = error.to_string();
ui_state.set_connect_provider_form_error(message.clone());
ui_state.status = message;
return;
}
};
let api_key_env_var =
match crate::config::validate_optional_env_var_name(connect.api_key_env_var.value()) {
Ok(value) => value,
Err(error) => {
let message = error.to_string();
ui_state.set_connect_provider_form_error(message.clone());
ui_state.status = message;
return;
}
};
if let Some(active) = ui_state.modals.connect_provider.as_mut() {
active.provider_id = Some(provider_id.clone());
active.label.set_value(label.clone());
active.base_url.set_value(base_url.clone());
active
.api_key_env_var
.set_value(api_key_env_var.clone().unwrap_or_default());
active.error = None;
}
let settings = match crate::config::read_settings(&self.config.paths) {
Ok(settings) => settings,
Err(error) => {
let message = format!("could not read provider settings: {error}");
ui_state.set_connect_provider_form_error(message.clone());
ui_state.status = message;
return;
}
};
if let Some(existing) = settings.custom_providers.get(&provider_id) {
ui_state.open_connect_provider_replacement_confirmation(
provider_id.clone(),
existing.label.clone(),
label,
base_url,
api_key_env_var,
);
ui_state.status = format!(
"custom provider '{provider_id}' already exists; press [Enter] to replace or [Esc] to cancel"
);
return;
}
self.start_custom_provider_save_worker(
provider_id,
label,
base_url,
api_key_env_var.unwrap_or_default(),
ui_state,
);
}
fn start_custom_provider_save_worker(
&mut self,
provider_id: String,
label: String,
base_url: String,
api_key_env_var: String,
ui_state: &mut state::MissionControlState,
) {
if self.active_run || self.worker.is_some() {
let message = "cannot save provider configuration while another worker is running";
ui_state.set_connect_provider_form_error(message);
ui_state.status = message.to_string();
return;
}
ui_state.set_connect_provider_saving(provider_id.clone());
ui_state.status = "saving provider configuration…".to_string();
let paths = self.config.paths.clone();
let sender = self.events.clone();
let cancel = Arc::new(AtomicBool::new(false));
let outcome = Arc::new(WorkerOutcomeState::default());
let worker_id = outcome.worker_id();
ui_state.active_worker_id = Some(worker_id);
ui_state.last_run_finished_worker_id = None;
self.active_run = true;
let worker_cancel = Arc::clone(&cancel);
let worker_outcome = Arc::clone(&outcome);
let worker_provider_id = provider_id.clone();
let handle = thread::spawn(move || {
let result = if worker_cancel.load(Ordering::SeqCst) {
Err("custom provider configuration cancelled; settings unchanged".to_string())
} else {
panic::catch_unwind(panic::AssertUnwindSafe(|| {
crate::login::configure_custom_provider(
&paths,
&worker_provider_id,
&label,
&base_url,
&api_key_env_var,
)
.map(|result| result.message)
.map_err(|error| error.to_string())
}))
.unwrap_or_else(|_| Err("custom provider persistence worker panicked".to_string()))
};
send_completion(
&sender,
&worker_outcome,
Some(WorkerFinalEvent::CustomProviderFinished {
provider_id,
result,
}),
);
});
self.worker = Some(WorkerState {
handle,
cancel,
login_manual: None,
outcome,
outcome_reconciled: false,
shutdown_policy: WorkerShutdownPolicy::WaitForCompletion,
steering: crate::agent::steering::AgentSteering::new(),
accepts_steering: false,
});
}
pub(crate) fn retry_connect_provider(&mut self, ui_state: &mut state::MissionControlState) {
let Some(stage) = ui_state.prepare_connect_provider_retry() else {
ui_state.status = "nothing to retry".to_string();
return;
};
match stage {
state::ConnectProviderStage::OAuth => {
let Some(provider_id) = ui_state
.modals
.connect_provider
.as_ref()
.and_then(|connect| connect.provider_id.clone())
else {
let message = "OAuth provider is unavailable".to_string();
let _ = ui_state.set_connect_provider_error(message.clone());
ui_state.status = message;
return;
};
ui_state.start_connect_oauth(provider_id);
self.start_openai_codex_login_worker(ui_state);
}
state::ConnectProviderStage::AnthropicGuidance => {
ui_state.start_connect_anthropic_guidance();
ui_state.status = crate::login::anthropic_api_key_setup_guidance().to_string();
}
state::ConnectProviderStage::ConfigureCustom => {
ui_state.status =
"edit the custom provider fields and press [Enter] to save".to_string();
}
state::ConnectProviderStage::ChooseProvider => {
ui_state.status = "select provider".to_string();
}
_ => {
ui_state.status = "provider connection can no longer be retried".to_string();
}
}
}
pub(crate) fn cancel_connect_provider(&mut self, ui_state: &mut state::MissionControlState) {
let stage = ui_state.connect_provider_stage();
let cancellation_status = self.request_cancel_active_worker(ui_state);
ui_state.close_connect_provider();
ui_state.status = if let Some(status) = cancellation_status {
status
} else if stage == Some(state::ConnectProviderStage::Success) {
"provider connection closed".to_string()
} else {
"provider connection cancelled; settings unchanged".to_string()
};
}
pub(crate) fn refresh_runtime_after_custom_provider(
&mut self,
provider_id: &str,
ui_state: &mut state::MissionControlState,
) {
if !ui_state.take_connect_provider_runtime_refresh(provider_id) {
return;
}
let provider = self.config.provider_id().to_string();
let model = self
.config
.model
.clone()
.unwrap_or_else(|| crate::providers::DEFAULT_CODEX_MODEL.to_string());
match crate::config::load_effective_provider_selection(
&self.config.paths,
&provider,
&model,
) {
Ok(refreshed) => {
self.config = refreshed.clone();
self.summarizer.refresh_config(&self.config, &self.settings);
self.state.auth_state = refreshed.auth_state();
self.state.model = model.clone();
self.state.config = Some(refreshed.clone());
ui_state.provider = provider.clone();
ui_state.model = model.clone();
ui_state.provider_ready = self.state.auth_state.is_ready();
ui_state.thinking_levels = cached_thinking_levels_for_model(
&self.config.paths,
&provider,
&model,
crate::thinking::capability_scope_for_provider(
&self.config.custom_providers,
&provider,
),
);
ui_state.refresh_thinking_levels(
self.config.thinking_level,
ui_state.thinking_levels.clone(),
);
self.refresh_fast_mode_state(ui_state);
}
Err(error) => {
ui_state.status =
format!("provider metadata saved, but runtime refresh failed: {error}");
}
}
}
pub(crate) fn submit_connect_provider_fallback(
&mut self,
ui_state: &mut state::MissionControlState,
) {
let sender = match self.worker.as_ref() {
None => {
ui_state.status = "no provider connection worker is active".to_string();
return;
}
Some(worker) => match worker.login_manual.as_ref() {
Some(sender) => sender.clone(),
None => {
ui_state.status = "active worker is not an OAuth flow".to_string();
return;
}
},
};
let Some(input) = ui_state
.modals
.connect_provider
.as_ref()
.map(|connect| connect.fallback_input.value().to_string())
.filter(|input| !input.trim().is_empty())
else {
return;
};
if sender.send(input).is_ok() {
let _ = ui_state.take_connect_provider_fallback_input();
ui_state.status = "manual OAuth redirect submitted; exchanging token…".to_string();
} else {
ui_state.status = "OAuth worker is no longer accepting fallback input".to_string();
}
}
pub(crate) fn confirm_connect_provider_replacement(
&mut self,
ui_state: &mut state::MissionControlState,
) {
let Some(replacement) = ui_state
.modals
.connect_provider
.as_ref()
.and_then(|connect| connect.replacement.clone())
else {
ui_state.status = "no custom provider replacement pending".to_string();
return;
};
self.start_custom_provider_save_worker(
replacement.provider_id,
replacement.label,
replacement.base_url,
replacement.api_key_env_var.unwrap_or_default(),
ui_state,
);
}
}