diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index ff218f408e02..c89986ff551a 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -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> { + 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::()?; + query = input.trim().to_string(); + + let filtered: Vec = 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> { + 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 = 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::>(), + ) + .interact()? + .to_string()) + } +} + +/// Dialog for configuring the A provider and model pub async fn configure_provider_dialog() -> Result> { // Get global config instance let config = Config::global(); @@ -427,16 +566,7 @@ pub async fn configure_provider_dialog() -> Result> { 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::>(), - ) - .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());