refactor(search): unify dropdown filtering on the shared ranked matcher

Branch, project, agent, model, provider, stash, SSH-host, skill-catalog
and archive filters each had their own toLowerCase().includes (or no
ordering at all); the git branch and gitmoji pickers also let cmdk
re-filter and reorder on top of the manual filter, silently dropping
rows. All of them now go through rankByQuery/matchesRankQuery: results
are relevance-ordered, multi-word queries match in any order, matching
ignores punctuation, and cmdk filtering is disabled where the ranked
list is already final. rankBranchesForQuery keeps relevance order
instead of re-sorting matches alphabetically; the model picker now also
matches model ids.
This commit is contained in:
Bohdan Triapitsyn
2026-08-24 15:00:52 +03:00
parent 081056e6e5
commit b8716fe808
19 changed files with 145 additions and 262 deletions
@@ -12,6 +12,7 @@ import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy }
import { CSS as DndCSS } from '@dnd-kit/utilities';
import { Icon } from '@/components/icon/Icon';
import { Input } from '@/components/ui/input';
import { matchesRankQuery } from '@/lib/search/fuzzySearch';
import { ProviderLogo } from '@/components/ui/ProviderLogo';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
@@ -455,18 +456,18 @@ export const ModelPickerList: React.FC<ModelPickerListProps> = ({
return hiddenModels.some((hidden) => hidden.providerID === providerID && hidden.modelID === modelID);
}, [hiddenModels]);
const matchesQuery = React.useCallback((modelName: string, providerName: string) => {
const query = searchQuery.trim().toLowerCase();
if (!query) return true;
return modelName.toLowerCase().includes(query) || providerName.toLowerCase().includes(query);
}, [searchQuery]);
const matchesQuery = React.useCallback(
(modelName: string, providerName: string, modelID?: string) =>
matchesRankQuery([modelName, modelID, providerName], searchQuery),
[searchQuery],
);
const filteredFavorites = React.useMemo(() => favoriteModels.filter(({ model, providerID, modelID }) => {
if (allowedProviderSet && !allowedProviderSet.has(providerID)) return false;
if (isModelAllowed && !isModelAllowed(providerID, modelID)) return false;
if (isHidden(providerID, modelID)) return false;
const providerName = providerById.get(providerID)?.name || providerID;
return matchesQuery(getModelDisplayName(model), providerName);
return matchesQuery(getModelDisplayName(model), providerName, modelID);
}), [allowedProviderSet, favoriteModels, isHidden, isModelAllowed, matchesQuery, providerById]);
const filteredRecents = React.useMemo(() => recentModels.filter(({ model, providerID, modelID }) => {
@@ -474,7 +475,7 @@ export const ModelPickerList: React.FC<ModelPickerListProps> = ({
if (isModelAllowed && !isModelAllowed(providerID, modelID)) return false;
if (isHidden(providerID, modelID)) return false;
const providerName = providerById.get(providerID)?.name || providerID;
return matchesQuery(getModelDisplayName(model), providerName);
return matchesQuery(getModelDisplayName(model), providerName, modelID);
}), [allowedProviderSet, isHidden, isModelAllowed, matchesQuery, providerById, recentModels]);
const orderedProviders = React.useMemo(() => {
@@ -495,7 +496,7 @@ export const ModelPickerList: React.FC<ModelPickerListProps> = ({
const modelID = typeof model.id === 'string' ? model.id : '';
if (!modelID || isHidden(provider.id, modelID)) return false;
if (isModelAllowed && !isModelAllowed(provider.id, modelID)) return false;
return matchesQuery(getModelDisplayName(model), provider.name || provider.id);
return matchesQuery(getModelDisplayName(model), provider.name || provider.id, modelID);
});
return { ...provider, models: filteredModels };
})