use anyhow::Result;
use vtcode_config::api_keys::{CredentialSource, provider_credential_detail};
use vtcode_config::auth::{AuthCredentialsStoreMode, CustomApiKeyStorage};
use vtcode_core::config::models::Provider;
use vtcode_core::utils::ansi::{AnsiRenderer, MessageStyle};
use super::common::prompt_with_placeholder;
use super::secret_input::{mask_key, read_secret_line};
pub(crate) fn prompt_api_key_interactive(renderer: &mut AnsiRenderer, provider: Provider) -> Result<()> {
if provider.is_local() {
renderer
.line(MessageStyle::Info, &format!("No API key required for {} (local provider).", provider.label()))?;
return Ok(());
}
if provider.uses_managed_auth() {
renderer.line(
MessageStyle::Info,
&format!(
"{} auth is managed by its official CLI. Run `vtcode login {}` to authenticate.",
provider.label(),
provider
),
)?;
return Ok(());
}
let env_key = provider.default_api_key_env();
let detail = provider_credential_detail(provider);
if let Some(discovered) = detail {
match discovered.source {
CredentialSource::Env => {
let var_name = discovered.env_var.unwrap_or(env_key);
renderer.line(MessageStyle::Status, &format!("Found {var_name} in your environment — using it."))?;
renderer
.line(MessageStyle::Info, "No key was written anywhere — switch providers anytime with /model.")?;
return Ok(());
}
CredentialSource::SecureStorage => {
renderer.line(
MessageStyle::Status,
&format!("Found a stored {} key in your OS keyring.", provider.label()),
)?;
if ask_replace_stored_key(renderer)? {
} else {
renderer.line(
MessageStyle::Info,
"Using the stored key. Switch providers or clear it anytime with /model.",
)?;
return Ok(());
}
}
CredentialSource::OAuth => {
renderer
.line(MessageStyle::Status, &format!("Using your active {} OAuth session.", provider.label()))?;
return Ok(());
}
CredentialSource::ManagedAuth | CredentialSource::Local => {
renderer.line(MessageStyle::Status, &format!("{} is ready.", provider.label()))?;
return Ok(());
}
}
}
prompt_paste_flow(renderer, provider, env_key)
}
fn ask_replace_stored_key(renderer: &mut AnsiRenderer) -> Result<bool> {
renderer.line(MessageStyle::Info, "Use the stored key (Enter) or replace it (r)?")?;
loop {
let input = prompt_with_placeholder("[Enter=use / r=replace]")?;
let trimmed = input.trim().to_ascii_lowercase();
match trimmed.as_str() {
"" | "u" | "use" => return Ok(false),
"r" | "replace" => return Ok(true),
other => {
renderer.line(
MessageStyle::Warning,
&format!("Unrecognized choice `{other}`. Press Enter to use the stored key, or `r` to replace it."),
)?;
}
}
}
}
fn prompt_paste_flow(renderer: &mut AnsiRenderer, provider: Provider, env_key: &'static str) -> Result<()> {
renderer.line(MessageStyle::Status, &format!("Set up your {} API key (env: {env_key}).", provider.label()))?;
renderer.line(
MessageStyle::Info,
"Paste your API key now (input is hidden), or press Enter to skip. You can also add it later with `/secret`.",
)?;
renderer.line(
MessageStyle::Info,
&format!(
"Tip: export {env_key} in your shell (e.g. ~/.zshrc) to skip this in every workspace — no duplication needed."
),
)?;
loop {
let prompt = format!("{} API key: ", provider.label());
let input = read_secret_line(&prompt)?;
let Some(key) = input else {
renderer.line(
MessageStyle::Info,
&format!("Skipped. Set {env_key} in your environment, or paste it later with /model."),
)?;
return Ok(());
};
if key.chars().any(|c| c.is_whitespace()) {
renderer.line(
MessageStyle::Warning,
"API key contains whitespace characters — this is likely a paste error.",
)?;
renderer.line(MessageStyle::Info, "Please re-enter the key without spaces, or press Enter to skip.")?;
continue;
}
if !confirm_save(renderer, provider, &key)? {
renderer.line(MessageStyle::Info, "Discarded. Re-enter the key, or press Enter to skip.")?;
continue;
}
let storage = CustomApiKeyStorage::new(provider.as_ref());
let mode = AuthCredentialsStoreMode::default();
storage
.store(&key, mode)
.map_err(|e| anyhow::anyhow!("Failed to store {} API key in OS keyring: {e}", provider.label()))?;
renderer.line(MessageStyle::Status, "API key saved to your OS keyring (not workspace environment files).")?;
renderer.line(
MessageStyle::Info,
&format!("To use the env var instead, export {env_key} in your shell and clear this key with /model."),
)?;
return Ok(());
}
}
fn confirm_save(renderer: &mut AnsiRenderer, provider: Provider, key: &str) -> Result<bool> {
renderer.line(
MessageStyle::Info,
&format!("{} key received: {} — length {} chars.", provider.label(), mask_key(key), key.chars().count()),
)?;
renderer.line(MessageStyle::Info, "Save to OS keyring? [Y]es (Enter) / [n]o / re-enter [r]")?;
loop {
let input = prompt_with_placeholder("[Enter=yes / n=no / r=re-enter]")?;
let trimmed = input.trim().to_ascii_lowercase();
match trimmed.as_str() {
"" | "y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
"r" | "re-enter" | "reenter" => return Ok(false),
other => {
renderer.line(
MessageStyle::Warning,
&format!("Unrecognized choice `{other}`. Press Enter to save, `n` to discard, or `r` to re-enter."),
)?;
}
}
}
}