use super::super::paths::ensure_config_dir;
use super::super::secrets::{decrypt_key, encrypt_key};
use super::super::types::ConfigFile;
use crate::common::{AgentError, Result};
pub const MAX_KEY_ATTEMPTS: u32 = 3;
pub fn wizard_select(title: &str, items: &[&str], default: usize) -> Result<usize> {
use super::super::super::config::wizard_style as s;
use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyEvent},
execute, terminal,
};
use std::io::Write;
let mut selected = default;
let mut stderr = std::io::stderr();
terminal::enable_raw_mode().map_err(|e| AgentError::Config(format!("Terminal error: {e}")))?;
let _ = execute!(stderr, cursor::Hide);
let lines_needed = items.len() as u16 + 3;
for _ in 0..lines_needed {
eprint!("\r\n");
}
let _ = execute!(stderr, cursor::MoveUp(lines_needed));
let result = (|| -> Result<usize> {
loop {
let _ = execute!(stderr, cursor::MoveToColumn(0));
eprint!("\r\x1b[J"); eprint!(" {}{}{}{}\r\n", s::BOLD, s::CYAN, title, s::RESET);
eprint!("\r\n");
for (i, item) in items.iter().enumerate() {
if i == selected {
eprint!(
" {}{}{} ▸ {} {}\r\n",
s::BG_CYAN,
s::BLACK,
s::BOLD,
item,
s::RESET
);
} else {
eprint!(" {} {} {}\r\n", s::DIM, item, s::RESET);
}
}
eprint!("\r\n");
eprint!(
" {}↑↓ navigate Enter select q cancel{}",
s::DIM,
s::RESET
);
stderr.flush().ok();
loop {
if let Ok(Event::Key(KeyEvent { code, .. })) = event::read() {
match code {
KeyCode::Up => {
selected = if selected == 0 {
items.len() - 1
} else {
selected - 1
};
break;
}
KeyCode::Down => {
selected = if selected == items.len() - 1 {
0
} else {
selected + 1
};
break;
}
KeyCode::Char('k') => {
selected = if selected == 0 {
items.len() - 1
} else {
selected - 1
};
break;
}
KeyCode::Char('j') => {
selected = if selected == items.len() - 1 {
0
} else {
selected + 1
};
break;
}
KeyCode::Enter => {
return Ok(selected);
}
KeyCode::Char('q') | KeyCode::Esc => {
return Ok(default);
}
_ => {}
}
}
}
let lines_up = items.len() as u16 + 3;
let _ = execute!(stderr, cursor::MoveUp(lines_up));
}
})();
let _ = execute!(stderr, cursor::Show);
terminal::disable_raw_mode().map_err(|e| AgentError::Config(format!("Terminal error: {e}")))?;
let _ = execute!(stderr, cursor::MoveToColumn(0));
eprint!("\r\x1b[J");
result
}
pub fn wizard_multi_select(title: &str, items: &[&str], configured: &[bool]) -> Result<Vec<usize>> {
use super::super::super::config::wizard_style as s;
use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyEvent},
execute, terminal,
};
use std::io::Write;
let mut cursor_pos: usize = 0;
let mut selected: Vec<bool> = (0..items.len())
.map(|i| configured.get(i).copied().unwrap_or(false))
.collect();
let mut stderr = std::io::stderr();
terminal::enable_raw_mode().map_err(|e| AgentError::Config(format!("Terminal error: {e}")))?;
let _ = execute!(stderr, cursor::Hide);
let lines_needed = items.len() as u16 + 3;
for _ in 0..lines_needed {
eprint!("\r\n");
}
let _ = execute!(stderr, cursor::MoveUp(lines_needed));
let result = (|| -> Result<Vec<usize>> {
loop {
let _ = execute!(stderr, cursor::MoveToColumn(0));
eprint!("\r\x1b[J");
eprint!(
" {}{}{} {}(Space to toggle, Enter to confirm){}\r\n",
s::BOLD,
s::CYAN,
title,
s::DIM,
s::RESET
);
eprint!("\r\n");
for (i, item) in items.iter().enumerate() {
let checkbox = if selected[i] {
format!("{}☑{}", s::GREEN, s::RESET)
} else {
format!("{}☐{}", s::DIM, s::RESET)
};
let is_configured = configured.get(i).copied().unwrap_or(false);
if i == cursor_pos {
eprint!(
" {} {}{}{} ▸ {} {}\r\n",
checkbox,
s::BG_CYAN,
s::BLACK,
s::BOLD,
item,
s::RESET
);
} else {
let name_style = if is_configured || selected[i] {
s::WHITE
} else {
s::DIM
};
eprint!(" {} {} {} {}\r\n", checkbox, name_style, item, s::RESET);
}
}
eprint!("\r\n");
eprint!(
" {}↑↓ navigate Space toggle Enter confirm q cancel{}",
s::DIM,
s::RESET
);
stderr.flush().ok();
loop {
if let Ok(Event::Key(KeyEvent { code, .. })) = event::read() {
match code {
KeyCode::Up | KeyCode::Char('k') => {
cursor_pos = if cursor_pos == 0 {
items.len() - 1
} else {
cursor_pos - 1
};
break;
}
KeyCode::Down | KeyCode::Char('j') => {
cursor_pos = if cursor_pos == items.len() - 1 {
0
} else {
cursor_pos + 1
};
break;
}
KeyCode::Char(' ') => {
selected[cursor_pos] = !selected[cursor_pos];
break;
}
KeyCode::Enter => {
if selected.iter().any(|&s| s) {
return Ok(selected
.iter()
.enumerate()
.filter_map(|(i, &s)| if s { Some(i) } else { None })
.collect());
}
break;
}
KeyCode::Char('q') | KeyCode::Esc => {
return Ok(vec![]);
}
_ => {}
}
}
}
let lines_up = items.len() as u16 + 3;
let _ = execute!(stderr, cursor::MoveUp(lines_up));
}
})();
let _ = execute!(stderr, cursor::Show);
terminal::disable_raw_mode().map_err(|e| AgentError::Config(format!("Terminal error: {e}")))?;
let _ = execute!(stderr, cursor::MoveToColumn(0));
eprint!("\r\x1b[J");
result
}
pub fn wizard_confirm(question: &str, default: bool) -> Result<bool> {
let items = ["Yes", "No"];
let default_idx = if default { 0 } else { 1 };
let idx = wizard_select(question, &items, default_idx)?;
Ok(idx == 0)
}
pub fn prompt_with_default(
label: &str,
default: &str,
input: &mut impl std::io::BufRead,
) -> Result<String> {
use super::super::super::config::wizard_style as s;
eprint!(
" {}{}{} {}[{}]{}: ",
s::BOLD,
s::WHITE,
label,
s::DIM,
default,
s::RESET
);
let mut line = String::new();
input.read_line(&mut line)?;
let trimmed = line.trim().to_string();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed)
}
}
pub(crate) fn prompt_api_key_inner(input: &mut impl std::io::BufRead) -> Result<String> {
for attempt in 0..MAX_KEY_ATTEMPTS {
let mut line = String::new();
input.read_line(&mut line)?;
let key = line.trim().to_string();
if !key.is_empty() {
return Ok(key);
}
if attempt < MAX_KEY_ATTEMPTS - 1 {
eprintln!("API key is required.");
}
}
Err(AgentError::Config("Failed to read API key.".to_string()))
}
pub fn prompt_api_key() -> Result<String> {
use std::io::IsTerminal;
if std::io::stdin().is_terminal() {
for attempt in 0..MAX_KEY_ATTEMPTS {
let key = rpassword::prompt_password("API key (required): ")
.map_err(|e| AgentError::Config(format!("Input error: {}", e)))?;
let key = key.trim().to_string();
if !key.is_empty() {
return Ok(key);
}
if attempt < MAX_KEY_ATTEMPTS - 1 {
eprintln!("API key is required.");
}
}
return Err(AgentError::Config("Failed to read API key.".to_string()));
}
prompt_api_key_inner(&mut std::io::BufReader::new(std::io::stdin()))
}
pub fn save_encrypted_key(key: &str) -> Result<()> {
let encrypted = encrypt_key(key)?;
let decrypted = decrypt_key(&encrypted)?;
if decrypted != key {
return Err(AgentError::Config(
"Encryption verification failed: decrypted value does not match original.".to_string(),
));
}
let dir = ensure_config_dir()?;
let path = dir.join("config.toml");
let mut cf = if path.exists() {
let content = std::fs::read_to_string(&path)?;
toml::from_str::<ConfigFile>(&content).unwrap_or_default()
} else {
ConfigFile::default()
};
cf.api.api_key_enc = Some(encrypted);
cf.api.api_key = None;
let toml_str = toml::to_string_pretty(&cf)
.map_err(|e| AgentError::Config(format!("Failed to serialize config: {e}")))?;
std::fs::write(&path, toml_str).map_err(|e| {
AgentError::Config(format!("Failed to write config {}: {e}", path.display()))
})?;
eprintln!("✅ API key encrypted and saved.");
Ok(())
}
pub fn save_web_credentials(username: Option<String>, password: &str) -> Result<()> {
let encrypted = encrypt_key(password)?;
let decrypted = decrypt_key(&encrypted)?;
if decrypted != password {
return Err(AgentError::Config(
"Encryption verification failed.".to_string(),
));
}
let dir = ensure_config_dir()?;
let path = dir.join("config.toml");
let mut cf = if path.exists() {
let content = std::fs::read_to_string(&path)?;
toml::from_str::<ConfigFile>(&content).unwrap_or_default()
} else {
ConfigFile::default()
};
cf.web.username = username;
cf.web.password_enc = Some(encrypted);
let toml_str = toml::to_string_pretty(&cf)
.map_err(|e| AgentError::Config(format!("Failed to serialize config: {e}")))?;
std::fs::write(&path, toml_str).map_err(|e| {
AgentError::Config(format!("Failed to write config {}: {e}", path.display()))
})?;
eprintln!("✅ Web credentials encrypted and saved.");
Ok(())
}
pub fn write_minimal_wizard_config(
path: &std::path::Path,
api_key_enc: &str,
base_url: &str,
model: &str,
) -> Result<()> {
let content = format!(
"[api]\napi_key_enc = {api_key_enc:?}\nbase_url = {base_url:?}\nmodel = {model:?}\n"
);
std::fs::write(path, content).map_err(|e| {
AgentError::Config(format!("Failed to write config {}: {e}", path.display()))
})?;
Ok(())
}
#[derive(Debug)]
pub enum WizardSignal<T> {
Next(T),
Back,
Cancel,
}
pub fn wizard_select_ws(
title: &str,
items: &[&str],
default: usize,
) -> Result<WizardSignal<usize>> {
use super::super::super::config::wizard_style as s;
use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyEvent},
execute, terminal,
};
use std::io::Write;
let mut selected = default;
let mut stderr = std::io::stderr();
terminal::enable_raw_mode().map_err(|e| AgentError::Config(format!("Terminal error: {e}")))?;
let _ = execute!(stderr, cursor::Hide);
let lines_needed = items.len() as u16 + 3;
for _ in 0..lines_needed {
eprint!("\r\n");
}
let _ = execute!(stderr, cursor::MoveUp(lines_needed));
let result = (|| -> Result<WizardSignal<usize>> {
loop {
let _ = execute!(stderr, cursor::MoveToColumn(0));
eprint!("\r\x1b[J");
eprint!(" {}{}{}{}\r\n", s::BOLD, s::CYAN, title, s::RESET);
eprint!("\r\n");
for (i, item) in items.iter().enumerate() {
if i == selected {
eprint!(
" {}{}{} ▸ {} {}\r\n",
s::BG_CYAN,
s::BLACK,
s::BOLD,
item,
s::RESET
);
} else {
eprint!(" {} {} {}\r\n", s::DIM, item, s::RESET);
}
}
eprint!("\r\n");
eprint!(
" {}↑↓ navigate Enter select Esc back q cancel{}",
s::DIM,
s::RESET
);
stderr.flush().ok();
loop {
if let Ok(Event::Key(KeyEvent { code, .. })) = event::read() {
match code {
KeyCode::Up | KeyCode::Char('k') => {
selected = if selected == 0 {
items.len() - 1
} else {
selected - 1
};
break;
}
KeyCode::Down | KeyCode::Char('j') => {
selected = if selected == items.len() - 1 {
0
} else {
selected + 1
};
break;
}
KeyCode::Enter => return Ok(WizardSignal::Next(selected)),
KeyCode::Esc => return Ok(WizardSignal::Back),
KeyCode::Char('q') => return Ok(WizardSignal::Cancel),
_ => {}
}
}
}
let lines_up = items.len() as u16 + 3;
let _ = execute!(stderr, cursor::MoveUp(lines_up));
}
})();
let _ = execute!(stderr, cursor::Show);
terminal::disable_raw_mode().map_err(|e| AgentError::Config(format!("Terminal error: {e}")))?;
let _ = execute!(stderr, cursor::MoveToColumn(0));
eprint!("\r\x1b[J");
result
}
pub fn apply_search_filter(
items: &[&str],
query: &str,
filtered: &mut Vec<usize>,
cursor: &mut usize,
offset: &mut usize,
) {
let q = query.to_lowercase();
*filtered = (0..items.len())
.filter(|&i| items[i].to_lowercase().contains(&q))
.collect();
*cursor = 0;
*offset = 0;
}
pub fn wizard_select_searchable(
title: &str,
items: &[&str],
default: usize,
) -> Result<WizardSignal<usize>> {
use super::super::super::config::wizard_style as s;
use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
execute, terminal,
};
use std::io::Write;
let mut stderr = std::io::stderr();
let term_height = terminal::size().map(|(_, h)| h as usize).unwrap_or(24);
let viewport_size = (term_height.saturating_sub(6)).clamp(3, 20);
let mut search_query = String::new();
let mut searching = false;
let mut filtered_indices: Vec<usize> = (0..items.len()).collect();
let mut cursor_pos: usize = filtered_indices
.iter()
.position(|&i| i == default)
.unwrap_or(0);
let mut scroll_offset: usize = cursor_pos.saturating_sub(viewport_size / 2);
terminal::enable_raw_mode().map_err(|e| AgentError::Config(format!("Terminal error: {e}")))?;
let _ = execute!(stderr, cursor::Hide);
let reserve = viewport_size as u16 + 5;
for _ in 0..reserve {
eprint!("\r\n");
}
let _ = execute!(stderr, cursor::MoveUp(reserve));
let result = (|| -> Result<WizardSignal<usize>> {
loop {
if cursor_pos < scroll_offset {
scroll_offset = cursor_pos;
}
if cursor_pos >= scroll_offset + viewport_size {
scroll_offset = cursor_pos + 1 - viewport_size;
}
let visible_end = (scroll_offset + viewport_size).min(filtered_indices.len());
let visible_start = scroll_offset.min(filtered_indices.len());
let visible = &filtered_indices[visible_start..visible_end];
let has_above = scroll_offset > 0;
let has_below = visible_end < filtered_indices.len();
let _ = execute!(stderr, cursor::MoveToColumn(0));
eprint!("\r\x1b[J");
if searching {
eprint!(
" {}{}{}{} {}/ {}{}\r\n",
s::BOLD,
s::CYAN,
title,
s::RESET,
s::YELLOW,
search_query,
s::RESET
);
} else {
eprint!(" {}{}{}{}\r\n", s::BOLD, s::CYAN, title, s::RESET);
}
if has_above {
eprint!(" {} ▲ {} more{}\r\n", s::DIM, scroll_offset, s::RESET);
} else {
eprint!("\r\n");
}
for (vi, &orig_idx) in visible.iter().enumerate() {
let abs_pos = scroll_offset + vi;
let label = items[orig_idx];
if abs_pos == cursor_pos {
eprint!(
" {}{}{} ▸ {} {}\r\n",
s::BG_CYAN,
s::BLACK,
s::BOLD,
label,
s::RESET
);
} else {
eprint!(" {} {} {}\r\n", s::DIM, label, s::RESET);
}
}
for _ in visible.len()..viewport_size {
eprint!("\r\n");
}
if has_below {
eprint!(
" {} ▼ {} more{}\r\n",
s::DIM,
filtered_indices.len() - visible_end,
s::RESET
);
} else {
eprint!("\r\n");
}
if filtered_indices.is_empty() {
eprint!(
" {}no matches Esc clear search q cancel{}",
s::DIM,
s::RESET
);
} else {
eprint!(
" {}{}/{} items ↑↓ navigate / search Enter select Esc back{}",
s::DIM,
cursor_pos + 1,
filtered_indices.len(),
s::RESET
);
}
stderr.flush().ok();
loop {
if let Ok(Event::Key(KeyEvent {
code, modifiers, ..
})) = event::read()
{
if searching {
match code {
KeyCode::Esc => {
searching = false;
search_query.clear();
filtered_indices = (0..items.len()).collect();
cursor_pos = filtered_indices
.iter()
.position(|&i| i == default)
.unwrap_or(0);
scroll_offset = cursor_pos.saturating_sub(viewport_size / 2);
break;
}
KeyCode::Enter => {
if !filtered_indices.is_empty() {
return Ok(WizardSignal::Next(filtered_indices[cursor_pos]));
}
break;
}
KeyCode::Backspace => {
search_query.pop();
apply_search_filter(
items,
&search_query,
&mut filtered_indices,
&mut cursor_pos,
&mut scroll_offset,
);
break;
}
KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => {
return Ok(WizardSignal::Cancel);
}
KeyCode::Char(c) => {
search_query.push(c);
apply_search_filter(
items,
&search_query,
&mut filtered_indices,
&mut cursor_pos,
&mut scroll_offset,
);
break;
}
KeyCode::Up => {
if !filtered_indices.is_empty() {
cursor_pos = if cursor_pos == 0 {
filtered_indices.len() - 1
} else {
cursor_pos - 1
};
}
break;
}
KeyCode::Down => {
if !filtered_indices.is_empty() {
cursor_pos = if cursor_pos >= filtered_indices.len() - 1 {
0
} else {
cursor_pos + 1
};
}
break;
}
_ => {}
}
} else {
match code {
KeyCode::Up | KeyCode::Char('k') => {
if !filtered_indices.is_empty() {
cursor_pos = if cursor_pos == 0 {
filtered_indices.len() - 1
} else {
cursor_pos - 1
};
}
break;
}
KeyCode::Down | KeyCode::Char('j') => {
if !filtered_indices.is_empty() {
cursor_pos = if cursor_pos >= filtered_indices.len() - 1 {
0
} else {
cursor_pos + 1
};
}
break;
}
KeyCode::Char('/') => {
searching = true;
break;
}
KeyCode::Enter => {
if !filtered_indices.is_empty() {
return Ok(WizardSignal::Next(filtered_indices[cursor_pos]));
}
break;
}
KeyCode::Esc => return Ok(WizardSignal::Back),
KeyCode::Char('q') => return Ok(WizardSignal::Cancel),
KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => {
return Ok(WizardSignal::Cancel);
}
_ => {}
}
}
}
}
let lines_up = viewport_size as u16 + 3;
let _ = execute!(stderr, cursor::MoveUp(lines_up));
}
})();
let _ = execute!(stderr, cursor::Show);
terminal::disable_raw_mode().map_err(|e| AgentError::Config(format!("Terminal error: {e}")))?;
let _ = execute!(stderr, cursor::MoveToColumn(0));
eprint!("\r\x1b[J");
result
}
pub fn wizard_multi_select_ws(
title: &str,
items: &[&str],
configured: &[bool],
) -> Result<WizardSignal<Vec<usize>>> {
use super::super::super::config::wizard_style as s;
use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyEvent},
execute, terminal,
};
use std::io::Write;
let mut cursor_pos: usize = 0;
let mut selected: Vec<bool> = (0..items.len())
.map(|i| configured.get(i).copied().unwrap_or(false))
.collect();
let mut stderr = std::io::stderr();
terminal::enable_raw_mode().map_err(|e| AgentError::Config(format!("Terminal error: {e}")))?;
let _ = execute!(stderr, cursor::Hide);
let lines_needed = items.len() as u16 + 3;
for _ in 0..lines_needed {
eprint!("\r\n");
}
let _ = execute!(stderr, cursor::MoveUp(lines_needed));
let result = (|| -> Result<WizardSignal<Vec<usize>>> {
loop {
let _ = execute!(stderr, cursor::MoveToColumn(0));
eprint!("\r\x1b[J");
eprint!(
" {}{}{} {}(Space to toggle, Enter to confirm){}\r\n",
s::BOLD,
s::CYAN,
title,
s::DIM,
s::RESET
);
eprint!("\r\n");
for (i, item) in items.iter().enumerate() {
let checkbox = if selected[i] {
format!("{}☑{}", s::GREEN, s::RESET)
} else {
format!("{}☐{}", s::DIM, s::RESET)
};
let is_configured = configured.get(i).copied().unwrap_or(false);
if i == cursor_pos {
eprint!(
" {} {}{}{} ▸ {} {}\r\n",
checkbox,
s::BG_CYAN,
s::BLACK,
s::BOLD,
item,
s::RESET
);
} else {
let name_style = if is_configured || selected[i] {
s::WHITE
} else {
s::DIM
};
eprint!(" {} {} {} {}\r\n", checkbox, name_style, item, s::RESET);
}
}
eprint!("\r\n");
eprint!(
" {}↑↓ navigate Space toggle Enter confirm Esc back q cancel{}",
s::DIM,
s::RESET
);
stderr.flush().ok();
loop {
if let Ok(Event::Key(KeyEvent { code, .. })) = event::read() {
match code {
KeyCode::Up | KeyCode::Char('k') => {
cursor_pos = if cursor_pos == 0 {
items.len() - 1
} else {
cursor_pos - 1
};
break;
}
KeyCode::Down | KeyCode::Char('j') => {
cursor_pos = if cursor_pos == items.len() - 1 {
0
} else {
cursor_pos + 1
};
break;
}
KeyCode::Char(' ') => {
selected[cursor_pos] = !selected[cursor_pos];
break;
}
KeyCode::Enter => {
if selected.iter().any(|&s| s) {
return Ok(WizardSignal::Next(
selected
.iter()
.enumerate()
.filter_map(|(i, &s)| if s { Some(i) } else { None })
.collect(),
));
}
break; }
KeyCode::Esc => return Ok(WizardSignal::Back),
KeyCode::Char('q') => return Ok(WizardSignal::Cancel),
_ => {}
}
}
}
let lines_up = items.len() as u16 + 3;
let _ = execute!(stderr, cursor::MoveUp(lines_up));
}
})();
let _ = execute!(stderr, cursor::Show);
terminal::disable_raw_mode().map_err(|e| AgentError::Config(format!("Terminal error: {e}")))?;
let _ = execute!(stderr, cursor::MoveToColumn(0));
eprint!("\r\x1b[J");
result
}
pub fn wizard_confirm_ws(question: &str, default: bool) -> Result<WizardSignal<bool>> {
match wizard_select_ws(question, &["Yes", "No"], if default { 0 } else { 1 })? {
WizardSignal::Next(i) => Ok(WizardSignal::Next(i == 0)),
WizardSignal::Back => Ok(WizardSignal::Back),
WizardSignal::Cancel => Ok(WizardSignal::Cancel),
}
}
pub fn wizard_prompt_ws(label: &str, default: &str) -> Result<WizardSignal<String>> {
use super::super::super::config::wizard_style as s;
use crossterm::{
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
terminal,
};
use std::io::Write;
eprint!(
" {}{}{} {}[{}]{}: ",
s::BOLD,
s::WHITE,
label,
s::DIM,
default,
s::RESET
);
std::io::stderr().flush().ok();
let mut buf = String::new();
terminal::enable_raw_mode().map_err(|e| AgentError::Config(format!("Terminal error: {e}")))?;
let result = (|| -> Result<WizardSignal<String>> {
loop {
if let Ok(Event::Key(KeyEvent {
code, modifiers, ..
})) = event::read()
{
match code {
KeyCode::Enter => {
let val = if buf.is_empty() {
default.to_string()
} else {
buf.clone()
};
return Ok(WizardSignal::Next(val));
}
KeyCode::Esc => return Ok(WizardSignal::Back),
KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => {
return Ok(WizardSignal::Cancel);
}
KeyCode::Backspace => {
if buf.pop().is_some() {
eprint!("\x08 \x08");
std::io::stderr().flush().ok();
}
}
KeyCode::Char(c) => {
buf.push(c);
eprint!("{c}");
std::io::stderr().flush().ok();
}
_ => {}
}
}
}
})();
terminal::disable_raw_mode().map_err(|e| AgentError::Config(format!("Terminal error: {e}")))?;
eprint!("\r\n");
result
}
#[derive(Clone)]
pub struct WizardProvider {
pub name: String,
pub base_url: String,
pub model: String,
pub extra_models: Vec<String>,
pub api_key_enc: String,
}
pub fn wizard_configure_provider(
preset: &super::presets::ProviderPreset,
registry: &crate::registry::model::ModelRegistry,
) -> Result<WizardSignal<WizardProvider>> {
use super::super::super::config::wizard_style as s;
use super::presets::provider_short_name;
eprintln!();
eprintln!(
" {}{}── Configuring: {} ──{}",
s::BOLD,
s::CYAN,
preset.label,
s::RESET
);
let api_key = if !preset.api_key_required {
eprintln!(
" {}No API key required (local server).{}",
s::DIM,
s::RESET
);
String::new()
} else {
let env_key = if !preset.env_key_hint.is_empty() {
std::env::var(preset.env_key_hint)
.ok()
.filter(|v| !v.is_empty())
} else {
None
};
if let Some(ref found_key) = env_key {
let masked = if found_key.len() > 8 {
let start: String = found_key.chars().take(4).collect();
let end: String = found_key
.chars()
.rev()
.take(4)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
format!("{start}...{end}")
} else {
"****".to_string()
};
eprintln!(
" API key found in env: {}{}{} ({})",
s::CYAN,
preset.env_key_hint,
s::RESET,
masked
);
eprintln!(
" {}Press Enter to use it, or type a new key.{}",
s::DIM,
s::RESET
);
let entered = rpassword::prompt_password(" API key: ")
.map_err(|e| AgentError::Config(format!("Input error: {}", e)))?;
let entered = entered.trim().to_string();
if entered.is_empty() {
eprintln!(" {}✓{} Using env key.", s::GREEN, s::RESET);
found_key.clone()
} else {
eprintln!(
" {}✓{} Key will be encrypted and stored.",
s::GREEN,
s::RESET
);
entered
}
} else {
if !preset.env_key_hint.is_empty() {
eprintln!(
" {}Hint: set {} env var for auto-detection{}",
s::DIM,
preset.env_key_hint,
s::RESET
);
}
let key = prompt_api_key()?;
eprintln!(
" {}✓{} Key will be encrypted and stored.",
s::GREEN,
s::RESET
);
key
}
};
let base_url_default = if preset.base_url.is_empty() {
"https://api.example.com/v1"
} else {
preset.base_url
};
let base_url = match wizard_prompt_ws("Base URL", base_url_default)? {
WizardSignal::Next(v) => v,
WizardSignal::Back => return Ok(WizardSignal::Back),
WizardSignal::Cancel => return Ok(WizardSignal::Cancel),
};
eprintln!(" {}✓{} Base URL: {}", s::GREEN, s::RESET, base_url);
let short_name = provider_short_name(preset.label);
let registry_models = registry.models_for_provider(&short_name);
let selected_models: Vec<String> = if !registry_models.is_empty() {
use crate::registry::model::ModelRegistry;
let reg_labels: Vec<String> = registry_models
.iter()
.map(|rm| {
let name = ModelRegistry::model_name(&rm.key);
let cost = match (rm.input_cost_per_million, rm.output_cost_per_million) {
(Some(i), Some(o)) => format!(" (${:.2} / ${:.2} per 1M)", i, o),
_ => String::new(),
};
let ctx = rm
.max_input_tokens
.map(|t| format!(" {}K ctx", t / 1000))
.unwrap_or_default();
format!("{name}{ctx}{cost}")
})
.collect();
let reg_label_refs: Vec<&str> = reg_labels.iter().map(|l| l.as_str()).collect();
let default_idx = registry_models
.iter()
.position(|rm| ModelRegistry::model_name(&rm.key) == preset.default_model)
.unwrap_or(0);
match wizard_select_searchable(
&format!("Model for {} (type to search)", short_name),
®_label_refs,
default_idx,
)? {
WizardSignal::Next(idx) => {
let name = ModelRegistry::model_name(®istry_models[idx].key).to_string();
vec![name]
}
WizardSignal::Back => return Ok(WizardSignal::Back),
WizardSignal::Cancel => return Ok(WizardSignal::Cancel),
}
} else if !preset.recommended_models.is_empty() {
let mut choices: Vec<&str> = preset.recommended_models.to_vec();
choices.push("(custom)");
let configured: Vec<bool> = choices.iter().map(|&m| m == preset.default_model).collect();
let indices =
match wizard_multi_select_ws("Models (Space to toggle)", &choices, &configured)? {
WizardSignal::Next(v) => v,
WizardSignal::Back => return Ok(WizardSignal::Back),
WizardSignal::Cancel => return Ok(WizardSignal::Cancel),
};
let mut models = Vec::new();
let custom_idx = choices.len() - 1;
for idx in &indices {
if *idx == custom_idx {
let input = match wizard_prompt_ws("Custom models (comma-separated)", "")? {
WizardSignal::Next(v) => v,
WizardSignal::Back => return Ok(WizardSignal::Back),
WizardSignal::Cancel => return Ok(WizardSignal::Cancel),
};
for m in input.split(',') {
let m = m.trim();
if !m.is_empty() {
models.push(m.to_string());
}
}
} else {
models.push(choices[*idx].to_string());
}
}
if models.is_empty() {
models.push(preset.default_model.to_string());
}
models
} else {
let default_model = if preset.default_model.is_empty() {
"gpt-4o"
} else {
preset.default_model
};
let input = match wizard_prompt_ws("Models (comma-separated)", default_model)? {
WizardSignal::Next(v) => v,
WizardSignal::Back => return Ok(WizardSignal::Back),
WizardSignal::Cancel => return Ok(WizardSignal::Cancel),
};
input
.split(',')
.map(|m| m.trim().to_string())
.filter(|m| !m.is_empty())
.collect()
};
let model = selected_models[0].clone();
let extra_models: Vec<String> = selected_models.iter().skip(1).cloned().collect();
eprintln!(
" {}✓{} Models: {}",
s::GREEN,
s::RESET,
std::iter::once(model.as_str())
.chain(extra_models.iter().map(|m| m.as_str()))
.collect::<Vec<_>>()
.join(", ")
);
let api_key_enc = if api_key.is_empty() {
String::new()
} else {
let encrypted = encrypt_key(&api_key)?;
let decrypted = decrypt_key(&encrypted)?;
if decrypted != api_key {
return Err(AgentError::Config(
"Encryption verification failed.".to_string(),
));
}
encrypted
};
if !preset.default_supports_tools {
eprintln!(
" {}⚠ Note: {} may not support tool use (function calling). \
Agentic tasks will be limited.{}",
s::DIM,
preset.label,
s::RESET
);
}
let name = provider_short_name(preset.label);
Ok(WizardSignal::Next(WizardProvider {
name,
base_url,
model,
extra_models,
api_key_enc,
}))
}