use super::{
sort_items_by_ascii_label, PickerAction, PickerBadge, PickerBadgeTone, PickerItem, UiPicker,
};
use rho_providers::{
auth::login_dispatch::ProviderAuthentication,
credentials::{CredentialError, CredentialStore},
model::catalog,
provider,
};
pub(super) const ALL_REFRESHABLE_PROVIDERS: &str = "all";
pub(super) enum LoginGroupNext {
Provider(String),
MethodPicker(Box<UiPicker>),
}
pub(super) fn login_group_picker() -> UiPicker {
let mut items = catalog::login_groups()
.into_iter()
.map(|group| PickerItem {
section: None,
label: group.prompt,
detail: None,
preview: None,
badge: None,
value: group.id,
})
.collect::<Vec<_>>();
sort_items_by_ascii_label(&mut items);
UiPicker::new(
"select provider to login",
"type regex filter, tab complete, up/down select, enter confirm, esc cancel",
items,
PickerAction::LoginGroup,
)
}
pub(super) fn login_group_next(group: catalog::LoginGroup) -> LoginGroupNext {
let picker = login_method_picker(group);
match picker.items.as_slice() {
[only] => LoginGroupNext::Provider(only.value.clone()),
_ => LoginGroupNext::MethodPicker(Box::new(picker)),
}
}
pub(super) fn login_method_picker(group: catalog::LoginGroup) -> UiPicker {
let title = format!("select {} login method", group.prompt);
let group_id = group.id.clone();
let mut items = group
.methods
.into_iter()
.map(|method| PickerItem {
section: None,
label: method.prompt,
detail: None,
preview: None,
badge: None,
value: method.target.auth,
})
.collect::<Vec<_>>();
items.extend(
super::claude_login::EXTERNAL_LOGIN_METHODS
.iter()
.filter(|method| method.group_id == group_id)
.map(|method| PickerItem {
section: None,
label: method.label.into(),
detail: Some(method.detail.into()),
preview: None,
badge: None,
value: method.value.into(),
}),
);
UiPicker::new(
title,
"type regex filter, tab complete, up/down select, enter confirm, esc cancel",
items,
PickerAction::LoginProvider,
)
}
pub(super) fn auth_mode_picker(
store: &dyn CredentialStore,
provider_name: &str,
active_auth: &str,
) -> rho_providers::credentials::CredentialResult<UiPicker> {
let Some(descriptor) = provider::provider_descriptor(provider_name) else {
return Ok(UiPicker::new(
"Switch active auth mode",
"esc back",
Vec::new(),
PickerAction::SwitchAuthMode,
));
};
let mut items = Vec::new();
for mode in descriptor.auth_modes() {
match ProviderAuthentication::has_credentials(store, mode.id) {
Ok(true) => {}
Ok(false) | Err(CredentialError::InvalidData(_)) => continue,
Err(error @ CredentialError::StoreUnavailable(_)) => return Err(error),
}
items.push(PickerItem {
section: None,
label: mode.login_label.into(),
detail: Some(format!(
"Use {} for {}.",
mode.login_label, descriptor.display_name
)),
preview: None,
badge: (mode.id == active_auth).then(|| PickerBadge {
text: "active".into(),
tone: PickerBadgeTone::Selected,
}),
value: mode.id.into(),
});
}
sort_items_by_ascii_label(&mut items);
Ok(UiPicker::new(
format!("Switch {} auth mode", descriptor.display_name),
"type regex filter, up/down select, enter switch, esc back",
items,
PickerAction::SwitchAuthMode,
)
.with_confirm_verb("switch"))
}
pub(super) fn refresh_model_list_picker(available_auths: &[String]) -> UiPicker {
let mut items = vec![PickerItem {
section: None,
label: "All configured providers".into(),
detail: Some("Refresh every available provider with model discovery support.".into()),
preview: None,
badge: None,
value: ALL_REFRESHABLE_PROVIDERS.into(),
}];
let mut providers = provider::providers()
.iter()
.filter(|descriptor| descriptor.model_refresh.is_some())
.filter(|descriptor| {
descriptor
.auth_modes()
.any(|mode| available_auths.iter().any(|auth| auth == mode.id))
})
.map(|descriptor| PickerItem {
section: None,
label: descriptor.display_name.into(),
detail: Some(format!(
"Refresh cached {} models.",
descriptor.display_name
)),
preview: None,
badge: None,
value: descriptor.name.into(),
})
.collect::<Vec<_>>();
sort_items_by_ascii_label(&mut providers);
items.extend(providers);
UiPicker::new(
"Refresh model lists",
"type regex filter, enter refresh, esc back",
items,
PickerAction::RefreshModelList,
)
}
pub(super) fn logout_provider_picker(
store: &dyn CredentialStore,
claude_signed_in: bool,
) -> rho_providers::credentials::CredentialResult<UiPicker> {
let mut targets = Vec::new();
for target in catalog::login_targets() {
if ProviderAuthentication::has_stored_credentials(store, &target.auth)? {
targets.push(target);
}
}
let mut picker = provider_picker_for_targets("logout", PickerAction::LogoutProvider, targets);
if claude_signed_in {
picker.items.push(PickerItem {
section: None,
label: super::claude_login::CLAUDE_CODE_TARGET.into(),
detail: Some("Sign out of Claude Code everywhere the claude binary is used.".into()),
preview: None,
badge: None,
value: super::claude_login::CLAUDE_CODE_TARGET.into(),
});
sort_items_by_ascii_label(&mut picker.items);
}
Ok(picker)
}
fn provider_picker_for_targets(
verb: &str,
action: PickerAction,
targets: Vec<catalog::LoginTarget>,
) -> UiPicker {
let mut items = targets
.into_iter()
.map(|target| {
let multi_mode = provider::provider_descriptor(&target.provider)
.map(|descriptor| {
descriptor
.auth_modes
.iter()
.filter(|mode| mode.auth_kind != provider::ProviderAuthKind::None)
.count()
> 1
})
.unwrap_or(false);
let label = if multi_mode {
format!("{} ยท {}", target.provider, target.label)
} else {
target.provider.clone()
};
PickerItem {
section: None,
label,
detail: Some(target.label),
preview: None,
badge: None,
value: target.auth,
}
})
.collect::<Vec<_>>();
sort_items_by_ascii_label(&mut items);
UiPicker::new(
format!("select provider to {verb}"),
"type regex filter, tab complete, up/down select, enter confirm, esc cancel",
items,
action,
)
}
#[cfg(test)]
#[path = "provider_picker_tests.rs"]
mod tests;