Skip to content

Commit 47e8603

Browse files
arimxyerclaude
andcommitted
feat: filter provider list by search query and active filters in Models tab
Provider list now dynamically hides providers with zero matching models when search or capability filters are active, and shows filtered counts instead of total counts. Selected provider is preserved when still visible, otherwise falls back to "All". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1816b38 commit 47e8603

2 files changed

Lines changed: 97 additions & 55 deletions

File tree

src/tui/models/app.rs

Lines changed: 93 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub struct Filters {
4646
pub enum ProviderListItem {
4747
All,
4848
CategoryHeader(ProviderCategory),
49-
Provider(usize), // Index into providers
49+
Provider(usize, usize), // (index into providers, match count)
5050
}
5151

5252
#[derive(Debug, Clone)]
@@ -118,15 +118,40 @@ impl ModelsApp {
118118
providers: &'a [(String, Provider)],
119119
) -> Option<&'a (String, Provider)> {
120120
match self.provider_list_items.get(self.selected_provider) {
121-
Some(ProviderListItem::Provider(idx)) => providers.get(*idx),
121+
Some(ProviderListItem::Provider(idx, _)) => providers.get(*idx),
122122
_ => None,
123123
}
124124
}
125125

126+
fn has_active_filters(&self) -> bool {
127+
!self.search_query.is_empty()
128+
|| self.filters.reasoning
129+
|| self.filters.tools
130+
|| self.filters.open_weights
131+
|| self.filters.free
132+
}
133+
134+
fn provider_match_count(&self, provider_id: &str, provider: &Provider) -> usize {
135+
let query_lower = self.search_query.to_lowercase();
136+
provider
137+
.models
138+
.iter()
139+
.filter(|(model_id, model)| {
140+
let search_matches = query_lower.is_empty()
141+
|| model_id.to_lowercase().contains(&query_lower)
142+
|| model.name.to_lowercase().contains(&query_lower)
143+
|| provider_id.to_lowercase().contains(&query_lower);
144+
search_matches && self.passes_filters(model)
145+
})
146+
.count()
147+
}
148+
126149
pub fn update_provider_list(&mut self, providers: &[(String, Provider)]) {
127150
self.provider_list_items.clear();
128151
self.provider_list_items.push(ProviderListItem::All);
129152

153+
let filtering = self.has_active_filters();
154+
130155
if self.group_by_category {
131156
let categories = [
132157
ProviderCategory::Origin,
@@ -143,35 +168,55 @@ impl ModelsApp {
143168
continue;
144169
}
145170

146-
let mut indices: Vec<usize> = providers
171+
let mut items: Vec<(usize, usize)> = providers
147172
.iter()
148173
.enumerate()
149174
.filter(|(_, (id, _))| provider_category(id) == *cat)
150-
.map(|(idx, _)| idx)
175+
.filter_map(|(idx, (id, provider))| {
176+
let count = if filtering {
177+
let c = self.provider_match_count(id, provider);
178+
if c == 0 {
179+
return None;
180+
}
181+
c
182+
} else {
183+
provider.models.len()
184+
};
185+
Some((idx, count))
186+
})
151187
.collect();
152188

153-
if indices.is_empty() {
189+
if items.is_empty() {
154190
continue;
155191
}
156192

157-
indices.sort_by(|a, b| providers[*a].0.cmp(&providers[*b].0));
193+
items.sort_by(|a, b| providers[a.0].0.cmp(&providers[b.0].0));
158194

159195
self.provider_list_items
160196
.push(ProviderListItem::CategoryHeader(*cat));
161-
for idx in indices {
197+
for (idx, count) in items {
162198
self.provider_list_items
163-
.push(ProviderListItem::Provider(idx));
199+
.push(ProviderListItem::Provider(idx, count));
164200
}
165201
}
166202
} else {
167-
for (idx, (id, _)) in providers.iter().enumerate() {
203+
for (idx, (id, provider)) in providers.iter().enumerate() {
168204
if self.provider_category_filter != ProviderCategory::All
169205
&& provider_category(id) != self.provider_category_filter
170206
{
171207
continue;
172208
}
209+
let count = if filtering {
210+
let c = self.provider_match_count(id, provider);
211+
if c == 0 {
212+
continue;
213+
}
214+
c
215+
} else {
216+
provider.models.len()
217+
};
173218
self.provider_list_items
174-
.push(ProviderListItem::Provider(idx));
219+
.push(ProviderListItem::Provider(idx, count));
175220
}
176221
}
177222
}
@@ -365,20 +410,14 @@ impl ModelsApp {
365410
&self.filtered_models
366411
}
367412

368-
pub fn total_model_count(&self, providers: &[(String, Provider)]) -> usize {
369-
providers.iter().map(|(_, p)| p.models.len()).sum()
370-
}
371-
372-
pub fn filtered_model_count(&self, providers: &[(String, Provider)]) -> usize {
373-
if self.provider_category_filter == ProviderCategory::All {
374-
self.total_model_count(providers)
375-
} else {
376-
providers
377-
.iter()
378-
.filter(|(id, _)| provider_category(id) == self.provider_category_filter)
379-
.map(|(_, p)| p.models.len())
380-
.sum()
381-
}
413+
pub fn filtered_model_count(&self) -> usize {
414+
self.provider_list_items
415+
.iter()
416+
.filter_map(|item| match item {
417+
ProviderListItem::Provider(_, count) => Some(count),
418+
_ => None,
419+
})
420+
.sum()
382421
}
383422

384423
pub fn get_copy_full(&self) -> Option<String> {
@@ -554,34 +593,22 @@ impl ModelsApp {
554593

555594
pub fn toggle_reasoning(&mut self, providers: &[(String, Provider)]) {
556595
self.filters.reasoning = !self.filters.reasoning;
557-
self.selected_model = 0;
558-
self.update_filtered_models(providers);
559-
self.model_list_state.select(Some(self.selected_model + 1));
560-
self.reset_detail_scroll();
596+
self.rebuild_after_filter_change(providers);
561597
}
562598

563599
pub fn toggle_tools(&mut self, providers: &[(String, Provider)]) {
564600
self.filters.tools = !self.filters.tools;
565-
self.selected_model = 0;
566-
self.update_filtered_models(providers);
567-
self.model_list_state.select(Some(self.selected_model + 1));
568-
self.reset_detail_scroll();
601+
self.rebuild_after_filter_change(providers);
569602
}
570603

571604
pub fn toggle_open_weights(&mut self, providers: &[(String, Provider)]) {
572605
self.filters.open_weights = !self.filters.open_weights;
573-
self.selected_model = 0;
574-
self.update_filtered_models(providers);
575-
self.model_list_state.select(Some(self.selected_model + 1));
576-
self.reset_detail_scroll();
606+
self.rebuild_after_filter_change(providers);
577607
}
578608

579609
pub fn toggle_free(&mut self, providers: &[(String, Provider)]) {
580610
self.filters.free = !self.filters.free;
581-
self.selected_model = 0;
582-
self.update_filtered_models(providers);
583-
self.model_list_state.select(Some(self.selected_model + 1));
584-
self.reset_detail_scroll();
611+
self.rebuild_after_filter_change(providers);
585612
}
586613

587614
pub fn cycle_provider_category(&mut self, providers: &[(String, Provider)]) {
@@ -610,22 +637,40 @@ impl ModelsApp {
610637

611638
pub fn search_input(&mut self, c: char, providers: &[(String, Provider)]) {
612639
self.search_query.push(c);
613-
self.selected_model = 0;
614-
self.update_filtered_models(providers);
615-
self.model_list_state.select(Some(self.selected_model + 1));
616-
self.reset_detail_scroll();
640+
self.rebuild_after_filter_change(providers);
617641
}
618642

619643
pub fn search_backspace(&mut self, providers: &[(String, Provider)]) {
620644
self.search_query.pop();
621-
self.selected_model = 0;
622-
self.update_filtered_models(providers);
623-
self.model_list_state.select(Some(self.selected_model + 1));
624-
self.reset_detail_scroll();
645+
self.rebuild_after_filter_change(providers);
625646
}
626647

627648
pub fn clear_search(&mut self, providers: &[(String, Provider)]) {
628649
self.search_query.clear();
650+
self.rebuild_after_filter_change(providers);
651+
}
652+
653+
/// Rebuild provider list and model list after any search/filter change.
654+
/// Preserves the selected provider if it's still visible, otherwise falls back to "All".
655+
fn rebuild_after_filter_change(&mut self, providers: &[(String, Provider)]) {
656+
// Remember which provider was selected (by index into providers slice)
657+
let prev_provider_idx = match self.provider_list_items.get(self.selected_provider) {
658+
Some(ProviderListItem::Provider(idx, _)) => Some(*idx),
659+
_ => None, // All or CategoryHeader
660+
};
661+
662+
self.update_provider_list(providers);
663+
664+
// Try to find the previously selected provider in the new list
665+
let new_pos = prev_provider_idx.and_then(|prev_idx| {
666+
self.provider_list_items.iter().position(
667+
|item| matches!(item, ProviderListItem::Provider(idx, _) if *idx == prev_idx),
668+
)
669+
});
670+
671+
self.selected_provider = new_pos.unwrap_or(0);
672+
self.provider_list_state
673+
.select(Some(self.selected_provider));
629674
self.selected_model = 0;
630675
self.update_filtered_models(providers);
631676
self.model_list_state.select(Some(self.selected_model + 1));

src/tui/models/render.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ fn draw_providers(f: &mut Frame, area: Rect, app: &mut App) {
187187
for item in &app.models_app.provider_list_items {
188188
match item {
189189
ProviderListItem::All => {
190-
let count = app.models_app.filtered_model_count(&app.providers);
190+
let count = app.models_app.filtered_model_count();
191191
let text = format!("All ({})", count);
192192
items.push(ListItem::new(text).style(Style::default().fg(Color::Green)));
193193
}
@@ -208,18 +208,15 @@ fn draw_providers(f: &mut Frame, area: Rect, app: &mut App) {
208208
.style(Style::default().fg(color).add_modifier(Modifier::BOLD)),
209209
);
210210
}
211-
ProviderListItem::Provider(idx) => {
212-
if let Some((id, provider)) = app.providers.get(*idx) {
211+
ProviderListItem::Provider(idx, count) => {
212+
if let Some((id, _)) = app.providers.get(*idx) {
213213
let cat = provider_category(id);
214214
let initial = &cat.short_label()[..1];
215215
let color = cat.color();
216216
let line = Line::from(vec![
217217
Span::styled(initial, Style::default().fg(color)),
218218
Span::raw(format!(" {} ", id)),
219-
Span::styled(
220-
format!("({})", provider.models.len()),
221-
Style::default().fg(Color::Gray),
222-
),
219+
Span::styled(format!("({})", count), Style::default().fg(Color::Gray)),
223220
]);
224221
items.push(ListItem::new(line));
225222
}

0 commit comments

Comments
 (0)