feat(model-picker): add search filter to Nous overlay model list

Nous returned 402 models in the recent perf capture (~496 KB of
JSON). The picker's existing top-bar search field already filters
the catalog list (`filteredModels`) but the Nous overlay path
showed all 402 unfiltered, making it nearly unusable.

Add `filteredNousModels` mirroring the `filteredModels` shape:
filters `nousModels` by case-insensitive substring match against
both `id` and `owned_by`. Updates the empty-state overlay so
"no matches" surfaces a different message from "no models
loaded" — the user knows the catalog is fine, the search just
didn't match.

User feedback: "we need a search in the model picker, some of
these lists are large and unorganized."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-05-05 12:38:30 +02:00
parent a193003842
commit f2ddcbbd60
@@ -344,7 +344,7 @@ struct ModelPickerSheet: View {
}
}
List(selection: $overlayModelID) {
ForEach(nousModels) { model in
ForEach(filteredNousModels) { model in
VStack(alignment: .leading, spacing: 2) {
Text(model.id)
.font(.system(.body, design: .monospaced))
@@ -360,12 +360,23 @@ struct ModelPickerSheet: View {
.listStyle(.inset)
.frame(minHeight: 220)
.overlay {
if nousModels.isEmpty && !nousIsRefreshing {
ContentUnavailableView(
"No models loaded",
systemImage: "cpu",
description: Text("Sign in to Nous Portal to load the catalog, or enter a model ID manually.")
)
if filteredNousModels.isEmpty && !nousIsRefreshing {
if nousModels.isEmpty {
ContentUnavailableView(
"No models loaded",
systemImage: "cpu",
description: Text("Sign in to Nous Portal to load the catalog, or enter a model ID manually.")
)
} else {
// Models loaded but the search filtered them all
// out. Different message so the user knows the
// catalog is fine, just their query didn't match.
ContentUnavailableView(
"No matches",
systemImage: "magnifyingglass",
description: Text("No models match \"\(searchText)\".")
)
}
}
}
if nousFetchedAt == nil && !nousModels.isEmpty {
@@ -577,6 +588,20 @@ struct ModelPickerSheet: View {
}
}
/// Same shape as `filteredModels` but for the Nous overlay path
/// (`nousModels` is `[NousModel]`, not `[HermesModelInfo]`).
/// Nous returned 402 models in the user's capture; without a
/// filter the picker is a flat unsearchable list. Reuses the
/// same `searchText` field so the user types once and both
/// paths respond.
private var filteredNousModels: [NousModel] {
guard !searchText.isEmpty else { return nousModels }
let q = searchText.lowercased()
return nousModels.filter {
$0.id.lowercased().contains(q) || ($0.owned_by ?? "").lowercased().contains(q)
}
}
private var isSelectedProviderOverlay: Bool {
providers.first(where: { $0.providerID == selectedProviderID })?.isOverlay ?? false
}