CLI: improve model selection ux (#4071)
This commit is contained in:
@@ -279,7 +279,146 @@ async fn handle_oauth_configuration(
|
||||
}
|
||||
}
|
||||
|
||||
/// Dialog for configuring the AI provider and model
|
||||
/// Interactive model search that truncates the list to improve UX
|
||||
fn interactive_model_search(models: &[String]) -> Result<String, Box<dyn std::error::Error>> {
|
||||
const MAX_VISIBLE: usize = 30;
|
||||
let mut query = String::new();
|
||||
|
||||
loop {
|
||||
let _ = cliclack::clear_screen();
|
||||
|
||||
let _ = cliclack::log::info(format!(
|
||||
"🔍 {} models available. Type to filter.",
|
||||
models.len()
|
||||
));
|
||||
|
||||
let input: String = cliclack::input("Filtering models, press Enter to search")
|
||||
.placeholder("e.g., gpt, sonnet, llama, qwen")
|
||||
.default_input(&query)
|
||||
.interact::<String>()?;
|
||||
query = input.trim().to_string();
|
||||
|
||||
let filtered: Vec<String> = if query.is_empty() {
|
||||
models.to_vec()
|
||||
} else {
|
||||
let q = query.to_lowercase();
|
||||
models
|
||||
.iter()
|
||||
.filter(|m| m.to_lowercase().contains(&q))
|
||||
.cloned()
|
||||
.collect()
|
||||
};
|
||||
|
||||
if filtered.is_empty() {
|
||||
let _ = cliclack::log::warning("No matching models. Try a different search.");
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut items: Vec<(String, String, &str)> = filtered
|
||||
.iter()
|
||||
.take(MAX_VISIBLE)
|
||||
.map(|m| (m.clone(), m.clone(), ""))
|
||||
.collect();
|
||||
|
||||
if filtered.len() > MAX_VISIBLE {
|
||||
items.insert(
|
||||
0,
|
||||
(
|
||||
"__refine__".to_string(),
|
||||
format!(
|
||||
"Refine search to see more (showing {} of {} results)",
|
||||
MAX_VISIBLE,
|
||||
filtered.len()
|
||||
),
|
||||
"Too many matches",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
items.insert(
|
||||
0,
|
||||
(
|
||||
"__new_search__".to_string(),
|
||||
"Start a new search...".to_string(),
|
||||
"Enter a different search term",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let selection = cliclack::select("Select a model:")
|
||||
.items(&items)
|
||||
.interact()?;
|
||||
|
||||
if selection == "__refine__" {
|
||||
continue;
|
||||
} else if selection == "__new_search__" {
|
||||
query.clear();
|
||||
continue;
|
||||
} else {
|
||||
return Ok(selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_model_from_list(
|
||||
models: &[String],
|
||||
provider_meta: &goose::providers::base::ProviderMetadata,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
const MAX_MODELS: usize = 10;
|
||||
// Smart model selection:
|
||||
// If we have more than MAX_MODELS models, show the recommended models with additional search option.
|
||||
// Otherwise, show all models without search.
|
||||
|
||||
if models.len() > MAX_MODELS {
|
||||
// Get recommended models from provider metadata
|
||||
let recommended_models: Vec<String> = provider_meta
|
||||
.known_models
|
||||
.iter()
|
||||
.map(|m| m.name.clone())
|
||||
.filter(|name| models.contains(name))
|
||||
.collect();
|
||||
|
||||
if !recommended_models.is_empty() {
|
||||
let mut model_items: Vec<(String, String, &str)> = recommended_models
|
||||
.iter()
|
||||
.map(|m| (m.clone(), m.clone(), "Recommended"))
|
||||
.collect();
|
||||
|
||||
model_items.insert(
|
||||
0,
|
||||
(
|
||||
"search_all".to_string(),
|
||||
"Search all models...".to_string(),
|
||||
"Search complete model list",
|
||||
),
|
||||
);
|
||||
|
||||
let selection = cliclack::select("Select a model:")
|
||||
.items(&model_items)
|
||||
.interact()?;
|
||||
|
||||
if selection == "search_all" {
|
||||
Ok(interactive_model_search(models)?)
|
||||
} else {
|
||||
Ok(selection)
|
||||
}
|
||||
} else {
|
||||
Ok(interactive_model_search(models)?)
|
||||
}
|
||||
} else {
|
||||
// just a few models, show all without search for better UX
|
||||
Ok(cliclack::select("Select a model:")
|
||||
.items(
|
||||
&models
|
||||
.iter()
|
||||
.map(|m| (m, m.as_str(), ""))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.interact()?
|
||||
.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Dialog for configuring the A provider and model
|
||||
pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
|
||||
// Get global config instance
|
||||
let config = Config::global();
|
||||
@@ -427,16 +566,7 @@ pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
|
||||
cliclack::outro(style(e.to_string()).on_red().white())?;
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(Some(models)) => cliclack::select("Select a model:")
|
||||
.items(
|
||||
&models
|
||||
.iter()
|
||||
.map(|m| (m, m.as_str(), ""))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.filter_mode() // enable "fuzzy search" filtering for the list of models
|
||||
.interact()?
|
||||
.to_string(),
|
||||
Ok(Some(models)) => select_model_from_list(&models, provider_meta)?,
|
||||
Ok(None) => {
|
||||
let default_model =
|
||||
std::env::var("GOOSE_MODEL").unwrap_or(provider_meta.default_model.clone());
|
||||
|
||||
Reference in New Issue
Block a user