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
+5 -8
View File
@@ -43,6 +43,7 @@ import { useThemeSystem } from '@/contexts/useThemeSystem';
import { getProjectLabel, normalizePath } from './mobilePaths';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useI18n } from '@/lib/i18n';
import { matchesRankQuery, rankByQuery } from '@/lib/search/fuzzySearch';
import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, ProjectIconImage } from '@/lib/projectMeta';
import { cn } from '@/lib/utils';
import {
@@ -188,11 +189,8 @@ const findExactProjectMatch = (projects: ProjectMeta[], directory: string): Proj
return projects.find((project) => projectMatchesExactDirectory(project, normalizedDirectory)) ?? null;
};
const sessionMatchesQuery = (session: Session, projectLabel: string, query: string): boolean => {
if (!query) return true;
const haystack = `${session.title ?? ''} ${session.id} ${getSessionDirectory(session)} ${projectLabel}`.toLowerCase();
return haystack.includes(query);
};
const sessionMatchesQuery = (session: Session, projectLabel: string, query: string): boolean =>
matchesRankQuery([session.title, session.id, getSessionDirectory(session), projectLabel], query);
const MobileProjectIcon: React.FC<{
project: Pick<ProjectMeta, 'id' | 'icon' | 'color' | 'iconImage' | 'iconBackground'>;
@@ -1355,7 +1353,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
const filteredNodes = React.useMemo(() => {
if (!normalizedQuery) return projectNodes;
return projectNodes.filter((node) => {
if (`${node.project.label} ${node.project.path}`.toLowerCase().includes(normalizedQuery)) return true;
if (matchesRankQuery([node.project.label, node.project.path], normalizedQuery)) return true;
return node.buckets.some((bucket) =>
bucket.sessions.some((session) => sessionMatchesQuery(session, node.project.label, normalizedQuery)),
);
@@ -1385,8 +1383,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
const searchProjectMatches = React.useMemo(() => {
if (!normalizedQuery) return [] as Array<ProjectMeta & { sessionCount: number }>;
return projectsMeta
.filter((project) => `${project.label} ${project.path}`.toLowerCase().includes(normalizedQuery))
return rankByQuery(projectsMeta, normalizedQuery, (project) => [project.label, project.path])
.map((project) => ({
...project,
sessionCount: sessions.filter((session) => {