diff --git a/web/app/components/goto-anything/actions/index.ts b/web/app/components/goto-anything/actions/index.ts index 0d4986f144..6f8bb9564c 100644 --- a/web/app/components/goto-anything/actions/index.ts +++ b/web/app/components/goto-anything/actions/index.ts @@ -214,8 +214,12 @@ export const searchAnything = async ( actionItem?: ActionItem, dynamicActions?: Record, ): Promise => { + const trimmedQuery = query.trim() + if (actionItem) { - const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim() + const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const prefixPattern = new RegExp(`^(${escapeRegExp(actionItem.key)}|${escapeRegExp(actionItem.shortcut)})\\s*`) + const searchTerm = trimmedQuery.replace(prefixPattern, '').trim() try { return await actionItem.search(query, searchTerm, locale) } @@ -225,10 +229,12 @@ export const searchAnything = async ( } } - if (query.startsWith('@') || query.startsWith('/')) + if (trimmedQuery.startsWith('@') || trimmedQuery.startsWith('/')) return [] const globalSearchActions = Object.values(dynamicActions || Actions) + // Exclude slash commands from general search results + .filter(action => action.key !== '/') // Use Promise.allSettled to handle partial failures gracefully const searchPromises = globalSearchActions.map(async (action) => { diff --git a/web/app/components/goto-anything/index.tsx b/web/app/components/goto-anything/index.tsx index 5cdf970725..50eddd1a43 100644 --- a/web/app/components/goto-anything/index.tsx +++ b/web/app/components/goto-anything/index.tsx @@ -177,31 +177,42 @@ const GotoAnything: FC = ({ } }, [router]) + const dedupedResults = useMemo(() => { + const seen = new Set() + return searchResults.filter((result) => { + const key = `${result.type}-${result.id}` + if (seen.has(key)) + return false + seen.add(key) + return true + }) + }, [searchResults]) + // Group results by type - const groupedResults = useMemo(() => searchResults.reduce((acc, result) => { + const groupedResults = useMemo(() => dedupedResults.reduce((acc, result) => { if (!acc[result.type]) acc[result.type] = [] acc[result.type].push(result) return acc }, {} as { [key: string]: SearchResult[] }), - [searchResults]) + [dedupedResults]) useEffect(() => { if (isCommandsMode) return - if (!searchResults.length) + if (!dedupedResults.length) return - const currentValueExists = searchResults.some(result => `${result.type}-${result.id}` === cmdVal) + const currentValueExists = dedupedResults.some(result => `${result.type}-${result.id}` === cmdVal) if (!currentValueExists) - setCmdVal(`${searchResults[0].type}-${searchResults[0].id}`) - }, [isCommandsMode, searchResults, cmdVal]) + setCmdVal(`${dedupedResults[0].type}-${dedupedResults[0].id}`) + }, [isCommandsMode, dedupedResults, cmdVal]) const emptyResult = useMemo(() => { - if (searchResults.length || !searchQuery.trim() || isLoading || isCommandsMode) + if (dedupedResults.length || !searchQuery.trim() || isLoading || isCommandsMode) return null const isCommandSearch = searchMode !== 'general' @@ -246,7 +257,7 @@ const GotoAnything: FC = ({ ) - }, [searchResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode]) + }, [dedupedResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode]) const defaultUI = useMemo(() => { if (searchQuery.trim()) @@ -430,14 +441,14 @@ const GotoAnything: FC = ({ {/* Always show footer to prevent height jumping */}
- {(!!searchResults.length || isError) ? ( + {(!!dedupedResults.length || isError) ? ( <> {isError ? ( {t('app.gotoAnything.someServicesUnavailable')} ) : ( <> - {t('app.gotoAnything.resultCount', { count: searchResults.length })} + {t('app.gotoAnything.resultCount', { count: dedupedResults.length })} {searchMode !== 'general' && ( {t('app.gotoAnything.inScope', { scope: searchMode.replace('@', '') })}