Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 141 additions & 11 deletions crates/goose-cli/src/commands/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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());
Expand Down
Loading