use super::super::*;
use super::MissionControlApp;
type AuthUpdate = Box<dyn FnOnce(&mut MissionControlApp, &mut state::MissionControlState) + Send>;
pub(super) struct AuthWorker {
handle: thread::JoinHandle<Result<AuthUpdate, String>>,
writes: bool,
pending_read: Option<AuthUpdate>,
}
impl std::fmt::Debug for AuthWorker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthWorker")
.field("writes", &self.writes)
.finish_non_exhaustive()
}
}
impl MissionControlApp {
fn start_auth_read(
&mut self,
ui_state: &mut state::MissionControlState,
work: impl FnOnce(crate::config::McPaths) -> anyhow::Result<AuthUpdate> + Send + 'static,
) {
if let Some(worker) = self.auth_worker.as_mut() {
if worker.writes {
ui_state.status = "authentication operation already running".into();
return;
}
let expected = ui_state.modals.connect_provider.clone();
let logout = ui_state.modals.logout_picker.clone();
let confirmation = ui_state.modals.logout_confirmation.clone();
worker.pending_read = Some(Box::new(move |app, ui| {
if ui.modals.connect_provider == expected
&& ui.modals.logout_picker == logout
&& ui.modals.logout_confirmation == confirmation
{
app.start_auth_read(ui, work);
}
}));
ui_state.status = "reading authentication settings…".into();
return;
}
let paths = self.config.paths.clone();
let sender = self.events.clone();
let connect = ui_state
.modals
.connect_provider
.as_ref()
.map(|connect| connect.stage);
let logout = ui_state.modals.logout_picker.is_some();
let confirmation = ui_state.modals.logout_confirmation.clone();
self.auth_worker = Some(AuthWorker {
writes: false,
pending_read: None,
handle: thread::spawn(move || {
let result = work(paths).map_err(|error| error.to_string());
let _ = sender.try_send(TuiEvent::WorkerOutcomeReady);
Ok(Box::new(
move |app: &mut MissionControlApp, ui: &mut state::MissionControlState| {
if ui
.modals
.connect_provider
.as_ref()
.map(|connect| connect.stage)
!= connect
|| ui.modals.logout_picker.is_some() != logout
|| ui.modals.logout_confirmation != confirmation
{
return;
}
match result {
Ok(update) => update(app, ui),
Err(error) => MissionControlApp::apply_ui_error(ui, error),
}
},
) as AuthUpdate)
}),
});
ui_state.status = "reading authentication settings…".into();
}
#[cfg(test)]
pub(crate) fn poll_auth_until_idle(&mut self, ui: &mut state::MissionControlState) -> bool {
self.reap_auth_worker(ui);
self.auth_worker.is_none()
}
pub(super) fn reap_auth_worker(&mut self, ui: &mut state::MissionControlState) -> bool {
if !self
.auth_worker
.as_ref()
.is_some_and(|worker| worker.handle.is_finished())
{
return false;
}
let worker = self
.auth_worker
.take()
.expect("finished authentication worker");
if worker.writes {
self.active_run = false;
}
match worker.handle.join() {
Ok(Ok(update)) => update(self, ui),
Ok(Err(error)) => Self::apply_ui_error(ui, error),
Err(_) => Self::apply_ui_error(ui, "authentication worker panicked"),
}
if let Some(pending) = worker.pending_read {
pending(self, ui);
}
true
}
pub(super) fn join_auth_worker_for_cleanup(&mut self) -> Vec<String> {
let Some(worker) = self.auth_worker.take() else {
return Vec::new();
};
if !worker.writes && !worker.handle.is_finished() {
return Vec::new();
}
match worker.handle.join() {
Ok(Ok(_)) => Vec::new(),
Ok(Err(error)) => vec![error],
Err(_) => vec!["authentication worker panicked".into()],
}
}
pub(crate) fn open_connect_provider(&mut self, ui: &mut state::MissionControlState) {
if self.auth_worker.is_some() {
ui.status = "authentication operation already running".into();
return;
}
let entries = crate::login::providers()
.into_iter()
.map(|provider| state::LoginProviderEntry {
id: provider.id.into(),
label: provider.label.into(),
description: provider.description.into(),
status: "loading…".into(),
})
.collect();
ui.open_connect_provider(entries, None);
self.start_auth_read(ui, |paths| {
let entries: anyhow::Result<Vec<_>> = crate::login::providers()
.into_iter()
.map(|provider| {
Ok(state::LoginProviderEntry {
id: provider.id.into(),
label: provider.label.into(),
description: provider.description.into(),
status: crate::login::login_provider_status_label(&paths, provider.id)?
.into(),
})
})
.collect();
let entries = entries?;
Ok(Box::new(move |_, ui| {
ui.open_connect_provider(entries, None);
ui.status = "select login provider".into();
}))
});
}
pub(crate) fn open_logout_picker(
&mut self,
ui: &mut state::MissionControlState,
area: ratatui::layout::Rect,
) {
if self.auth_worker.is_some() {
ui.status = "authentication operation already running".into();
return;
}
ui.open_logout_picker(Vec::new(), None, logout_picker_visible_rows(area, false));
self.start_auth_read(ui, move |paths| {
let mut entries = crate::login::providers()
.into_iter()
.map(|provider| {
Ok(state::LoginProviderEntry {
id: provider.id.into(),
label: provider.label.into(),
description: provider.description.into(),
status: crate::login::logout_provider_status(&paths, provider.id)?
.label()
.into(),
})
})
.collect::<anyhow::Result<Vec<_>>>()?;
entries.extend(
crate::config::read_settings(&paths)?
.custom_providers
.into_iter()
.map(|(id, provider)| state::LoginProviderEntry {
id,
label: provider.label,
description: "custom provider metadata".into(),
status: "configured".into(),
}),
);
Ok(Box::new(move |_, ui| {
ui.open_logout_picker(entries, None, logout_picker_visible_rows(area, false));
ui.status = "select logout provider".into();
}))
});
}
pub(crate) fn logout_provider(
&mut self,
provider_id: &str,
ui: &mut state::MissionControlState,
) {
if self.active_run || self.worker.is_some() {
Self::apply_ui_error(ui, "cannot logout while a prompt is running");
return;
}
let id = provider_id.to_string();
self.start_auth_read(ui, move |paths| {
let settings = crate::config::read_settings(&paths)?;
let label = if let Some(provider) = settings.custom_providers.get(&id) {
provider.label.clone()
} else {
let provider = crate::login::validate_logout_provider(&id)?;
if crate::login::logout_provider_status(&paths, &id)?
== crate::login::LogoutProviderStatus::Missing
{
return Ok(Box::new(
move |_: &mut MissionControlApp, ui: &mut state::MissionControlState| {
ui.status = format!("{id} is not configured; credentials unchanged");
},
) as AuthUpdate);
}
provider.label.to_string()
};
Ok(Box::new(move |app, ui| {
if app.active_run || app.worker.is_some() {
return;
}
ui.open_logout_confirmation(id.clone(), label);
ui.status = format!("confirm logout for {id}");
}))
});
}
pub(crate) fn remove_logout_provider(
&mut self,
provider_id: &str,
ui: &mut state::MissionControlState,
) {
if self.active_run || self.worker.is_some() || self.auth_worker.is_some() {
Self::apply_ui_error(ui, "cannot logout while another operation is running");
return;
}
let paths = self.config.paths.clone();
let id = provider_id.to_string();
let sender = self.events.clone();
self.active_run = true;
self.auth_worker = Some(AuthWorker {
writes: true,
pending_read: None,
handle: thread::spawn(move || {
let result = crate::commands::runtime::remove_logout_target(&paths, &id)
.map_err(|error| error.to_string());
let _ = sender.try_send(TuiEvent::WorkerOutcomeReady);
let removal = result?;
Ok(Box::new(
move |app: &mut MissionControlApp, ui: &mut state::MissionControlState| {
app.active_run = false;
if id == crate::providers::OPENAI_CODEX_PROVIDER {
app.codex_quota.invalidate();
ui.codex_quota_label = None;
}
crate::commands::runtime::reconcile_runtime_after_logout(
&mut app.state,
Some(&mut app.config),
&removal,
);
ui.provider_ready = app.state.auth_state.is_ready();
ui.status = removal.message();
},
) as AuthUpdate)
}),
});
ui.status = "removing provider credentials…".into();
}
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);
if ui_state.modals.connect_provider.is_none() {
return;
}
}
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 => {
self.codex_quota.invalidate();
ui_state.codex_quota_label = None;
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 expected = ui_state.modals.connect_provider.clone();
self.start_auth_read(ui_state, move |paths| {
let settings = crate::config::read_settings(&paths)?;
let existing = settings.custom_providers.get(&provider_id).map(|provider| provider.label.clone());
Ok(Box::new(move |app, ui| {
if ui.modals.connect_provider != expected { return; }
if let Some(existing) = existing {
ui.open_connect_provider_replacement_confirmation(provider_id.clone(), existing, label, base_url, api_key_env_var);
ui.status = format!("custom provider '{provider_id}' already exists; press [Enter] to replace or [Esc] to cancel");
} else {
app.start_custom_provider_save_worker(provider_id, label, base_url, api_key_env_var.unwrap_or_default(), ui);
}
}))
});
}
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,
);
}
}