Guide users through LLM setup with single-choice first provider

Start with a single-choice prompt for the first LLM (Anthropic, OpenAI,
or Gemini), then optionally offer a multi-select for additional providers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-10 16:00:09 -04:00
parent 8fc03130ef
commit 5e06cd788e

View file

@ -10,7 +10,7 @@ use arc_llm::provider::Provider;
use axum::extract::Query;
use axum::response::Html;
use axum::routing::get;
use dialoguer::{Confirm, Input, MultiSelect};
use dialoguer::{Confirm, Input, MultiSelect, Select};
use rand::Rng;
use tokio::net::TcpListener;
use tokio::sync::oneshot;
@ -285,6 +285,15 @@ fn prompt_input(prompt: &str) -> Result<String> {
)
}
fn prompt_select(prompt: &str, items: &[String]) -> Result<usize> {
Ok(
Select::with_theme(&dialoguer::theme::ColorfulTheme::default())
.with_prompt(prompt)
.items(items)
.interact_on(&dialoguer::console::Term::stderr())?,
)
}
fn prompt_multiselect(prompt: &str, items: &[String]) -> Result<Vec<usize>> {
Ok(
MultiSelect::with_theme(&dialoguer::theme::ColorfulTheme::default())
@ -561,31 +570,70 @@ pub async fn run_install() -> Result<()> {
// Step 1: LLM providers
eprintln!("[Step 1/4] LLM providers");
let provider_labels: Vec<String> = Provider::ALL
let mut env_pairs: Vec<(String, String)> = Vec::new();
let mut configured_providers: Vec<Provider> = Vec::new();
// First provider — single choice from the top 3
let primary_providers = [Provider::Anthropic, Provider::OpenAi, Provider::Gemini];
let primary_labels: Vec<String> = primary_providers
.iter()
.map(|p| {
let env_vars = p.api_key_env_vars().join(" / ");
format!("{} ({})", provider_display_name(*p), env_vars)
})
.map(|p| provider_display_name(*p).to_string())
.collect();
let selected_indices: Vec<usize> = tokio::task::spawn_blocking({
let labels = provider_labels.clone();
move || prompt_multiselect("Which LLM providers do you want to configure?", &labels)
let primary_idx: usize = tokio::task::spawn_blocking({
let labels = primary_labels.clone();
move || prompt_select("Choose your first LLM provider", &labels)
})
.await??;
let mut env_pairs: Vec<(String, String)> = Vec::new();
for idx in selected_indices {
let provider = Provider::ALL[idx];
let env_var = provider.api_key_env_vars()[0];
let url = provider_key_url(provider);
let first_provider = primary_providers[primary_idx];
{
let env_var = first_provider.api_key_env_vars()[0];
let url = provider_key_url(first_provider);
eprintln!(" Get your API key at: {url}");
let prompt = env_var.to_string();
let key: String = tokio::task::spawn_blocking(move || prompt_input(&prompt)).await??;
env_pairs.push((env_var.to_string(), key));
configured_providers.push(first_provider);
}
// Additional providers
let add_more =
tokio::task::spawn_blocking(|| prompt_confirm("Set up additional LLM providers?", false))
.await??;
if add_more {
let remaining_labels: Vec<String> = Provider::ALL
.iter()
.filter(|p| !configured_providers.contains(p))
.map(|p| {
let env_vars = p.api_key_env_vars().join(" / ");
format!("{} ({})", provider_display_name(*p), env_vars)
})
.collect();
let remaining_providers: Vec<Provider> = Provider::ALL
.iter()
.filter(|p| !configured_providers.contains(p))
.copied()
.collect();
let selected_indices: Vec<usize> = tokio::task::spawn_blocking({
let labels = remaining_labels.clone();
move || prompt_multiselect("Which additional LLM providers?", &labels)
})
.await??;
for idx in selected_indices {
let provider = remaining_providers[idx];
let env_var = provider.api_key_env_vars()[0];
let url = provider_key_url(provider);
eprintln!(" Get your API key at: {url}");
let prompt = env_var.to_string();
let key: String = tokio::task::spawn_blocking(move || prompt_input(&prompt)).await??;
env_pairs.push((env_var.to_string(), key));
}
}
eprintln!();