22// CodexLens Search Tab
33// ========================================
44// Semantic code search interface with multiple search types
5+ // Includes LSP availability check and hybrid search mode switching
56
67import { useState } from 'react' ;
78import { useIntl } from 'react-intl' ;
8- import { Search , FileCode , Code } from 'lucide-react' ;
9+ import { Search , FileCode , Code , Sparkles , CheckCircle , AlertTriangle } from 'lucide-react' ;
910import { Button } from '@/components/ui/Button' ;
1011import { Input } from '@/components/ui/Input' ;
1112import { Label } from '@/components/ui/Label' ;
@@ -20,11 +21,13 @@ import {
2021 useCodexLensSearch ,
2122 useCodexLensFilesSearch ,
2223 useCodexLensSymbolSearch ,
24+ useCodexLensLspStatus ,
25+ useCodexLensSemanticSearch ,
2326} from '@/hooks/useCodexLens' ;
24- import type { CodexLensSearchParams } from '@/lib/api' ;
27+ import type { CodexLensSearchParams , CodexLensSemanticSearchMode , CodexLensFusionStrategy } from '@/lib/api' ;
2528import { cn } from '@/lib/utils' ;
2629
27- type SearchType = 'search' | 'search_files' | 'symbol' ;
30+ type SearchType = 'search' | 'search_files' | 'symbol' | 'semantic' ;
2831type SearchMode = 'dense_rerank' | 'fts' | 'fuzzy' ;
2932
3033interface SearchTabProps {
@@ -35,14 +38,19 @@ export function SearchTab({ enabled }: SearchTabProps) {
3538 const { formatMessage } = useIntl ( ) ;
3639 const [ searchType , setSearchType ] = useState < SearchType > ( 'search' ) ;
3740 const [ searchMode , setSearchMode ] = useState < SearchMode > ( 'dense_rerank' ) ;
41+ const [ semanticMode , setSemanticMode ] = useState < CodexLensSemanticSearchMode > ( 'fusion' ) ;
42+ const [ fusionStrategy , setFusionStrategy ] = useState < CodexLensFusionStrategy > ( 'rrf' ) ;
3843 const [ query , setQuery ] = useState ( '' ) ;
3944 const [ hasSearched , setHasSearched ] = useState ( false ) ;
4045
46+ // LSP status check
47+ const lspStatus = useCodexLensLspStatus ( { enabled } ) ;
48+
4149 // Build search params based on search type
4250 const searchParams : CodexLensSearchParams = {
4351 query,
4452 limit : 20 ,
45- mode : searchType !== 'symbol' ? searchMode : undefined ,
53+ mode : searchType !== 'symbol' && searchType !== 'semantic' ? searchMode : undefined ,
4654 max_content_length : 200 ,
4755 extra_files_count : 10 ,
4856 } ;
@@ -63,12 +71,25 @@ export function SearchTab({ enabled }: SearchTabProps) {
6371 { enabled : enabled && hasSearched && searchType === 'symbol' && query . trim ( ) . length > 0 }
6472 ) ;
6573
74+ const semanticSearch = useCodexLensSemanticSearch (
75+ {
76+ query,
77+ mode : semanticMode ,
78+ fusion_strategy : semanticMode === 'fusion' ? fusionStrategy : undefined ,
79+ limit : 20 ,
80+ include_match_reason : true ,
81+ } ,
82+ { enabled : enabled && hasSearched && searchType === 'semantic' && query . trim ( ) . length > 0 }
83+ ) ;
84+
6685 // Get loading state based on search type
6786 const isLoading = searchType === 'search'
6887 ? contentSearch . isLoading
6988 : searchType === 'search_files'
7089 ? fileSearch . isLoading
71- : symbolSearch . isLoading ;
90+ : searchType === 'symbol'
91+ ? symbolSearch . isLoading
92+ : semanticSearch . isLoading ;
7293
7394 const handleSearch = ( ) => {
7495 if ( query . trim ( ) ) {
@@ -84,17 +105,52 @@ export function SearchTab({ enabled }: SearchTabProps) {
84105
85106 const handleSearchTypeChange = ( value : SearchType ) => {
86107 setSearchType ( value ) ;
87- setHasSearched ( false ) ; // Reset search state when changing type
108+ setHasSearched ( false ) ;
88109 } ;
89110
90111 const handleSearchModeChange = ( value : SearchMode ) => {
91112 setSearchMode ( value ) ;
92- setHasSearched ( false ) ; // Reset search state when changing mode
113+ setHasSearched ( false ) ;
114+ } ;
115+
116+ const handleSemanticModeChange = ( value : CodexLensSemanticSearchMode ) => {
117+ setSemanticMode ( value ) ;
118+ setHasSearched ( false ) ;
119+ } ;
120+
121+ const handleFusionStrategyChange = ( value : CodexLensFusionStrategy ) => {
122+ setFusionStrategy ( value ) ;
123+ setHasSearched ( false ) ;
93124 } ;
94125
95126 const handleQueryChange = ( value : string ) => {
96127 setQuery ( value ) ;
97- setHasSearched ( false ) ; // Reset search state when query changes
128+ setHasSearched ( false ) ;
129+ } ;
130+
131+ // Get result count for display
132+ const getResultCount = ( ) : string => {
133+ if ( searchType === 'symbol' ) {
134+ return symbolSearch . data ?. success
135+ ? `${ symbolSearch . data . symbols ?. length ?? 0 } ${ formatMessage ( { id : 'codexlens.search.resultsCount' } ) } `
136+ : '' ;
137+ }
138+ if ( searchType === 'search' ) {
139+ return contentSearch . data ?. success
140+ ? `${ contentSearch . data . results ?. length ?? 0 } ${ formatMessage ( { id : 'codexlens.search.resultsCount' } ) } `
141+ : '' ;
142+ }
143+ if ( searchType === 'search_files' ) {
144+ return fileSearch . data ?. success
145+ ? `${ fileSearch . data . files ?. length ?? 0 } ${ formatMessage ( { id : 'codexlens.search.resultsCount' } ) } `
146+ : '' ;
147+ }
148+ if ( searchType === 'semantic' ) {
149+ return semanticSearch . data ?. success
150+ ? `${ semanticSearch . data . count ?? 0 } ${ formatMessage ( { id : 'codexlens.search.resultsCount' } ) } `
151+ : '' ;
152+ }
153+ return '' ;
98154 } ;
99155
100156 if ( ! enabled ) {
@@ -115,6 +171,29 @@ export function SearchTab({ enabled }: SearchTabProps) {
115171
116172 return (
117173 < div className = "space-y-6" >
174+ { /* LSP Status Indicator */ }
175+ < div className = "flex items-center gap-2 text-sm" >
176+ < span className = "text-muted-foreground" > { formatMessage ( { id : 'codexlens.search.lspStatus' } ) } :</ span >
177+ { lspStatus . isLoading ? (
178+ < span className = "text-muted-foreground" > ...</ span >
179+ ) : lspStatus . available ? (
180+ < span className = "flex items-center gap-1 text-green-600 dark:text-green-400" >
181+ < CheckCircle className = "w-3.5 h-3.5" />
182+ { formatMessage ( { id : 'codexlens.search.lspAvailable' } ) }
183+ </ span >
184+ ) : ! lspStatus . semanticAvailable ? (
185+ < span className = "flex items-center gap-1 text-yellow-600 dark:text-yellow-400" >
186+ < AlertTriangle className = "w-3.5 h-3.5" />
187+ { formatMessage ( { id : 'codexlens.search.lspNoSemantic' } ) }
188+ </ span >
189+ ) : (
190+ < span className = "flex items-center gap-1 text-yellow-600 dark:text-yellow-400" >
191+ < AlertTriangle className = "w-3.5 h-3.5" />
192+ { formatMessage ( { id : 'codexlens.search.lspNoVector' } ) }
193+ </ span >
194+ ) }
195+ </ div >
196+
118197 { /* Search Options */ }
119198 < div className = "grid grid-cols-1 md:grid-cols-2 gap-4" >
120199 { /* Search Type */ }
@@ -143,12 +222,18 @@ export function SearchTab({ enabled }: SearchTabProps) {
143222 { formatMessage ( { id : 'codexlens.search.symbol' } ) }
144223 </ div >
145224 </ SelectItem >
225+ < SelectItem value = "semantic" disabled = { ! lspStatus . available } >
226+ < div className = "flex items-center gap-2" >
227+ < Sparkles className = "w-4 h-4" />
228+ { formatMessage ( { id : 'codexlens.search.semantic' } ) }
229+ </ div >
230+ </ SelectItem >
146231 </ SelectContent >
147232 </ Select >
148233 </ div >
149234
150- { /* Search Mode - only for content and file search */ }
151- { searchType !== 'symbol' && (
235+ { /* Search Mode - for CLI search types ( content / file) */ }
236+ { ( searchType === 'search' || searchType === 'search_files' ) && (
152237 < div className = "space-y-2" >
153238 < Label > { formatMessage ( { id : 'codexlens.search.mode' } ) } </ Label >
154239 < Select value = { searchMode } onValueChange = { handleSearchModeChange } >
@@ -169,8 +254,60 @@ export function SearchTab({ enabled }: SearchTabProps) {
169254 </ Select >
170255 </ div >
171256 ) }
257+
258+ { /* Semantic Search Mode - for semantic search type */ }
259+ { searchType === 'semantic' && (
260+ < div className = "space-y-2" >
261+ < Label > { formatMessage ( { id : 'codexlens.search.semanticMode' } ) } </ Label >
262+ < Select value = { semanticMode } onValueChange = { handleSemanticModeChange } >
263+ < SelectTrigger >
264+ < SelectValue />
265+ </ SelectTrigger >
266+ < SelectContent >
267+ < SelectItem value = "fusion" >
268+ { formatMessage ( { id : 'codexlens.search.semanticMode.fusion' } ) }
269+ </ SelectItem >
270+ < SelectItem value = "vector" >
271+ { formatMessage ( { id : 'codexlens.search.semanticMode.vector' } ) }
272+ </ SelectItem >
273+ < SelectItem value = "structural" >
274+ { formatMessage ( { id : 'codexlens.search.semanticMode.structural' } ) }
275+ </ SelectItem >
276+ </ SelectContent >
277+ </ Select >
278+ </ div >
279+ ) }
172280 </ div >
173281
282+ { /* Fusion Strategy - only when semantic + fusion mode */ }
283+ { searchType === 'semantic' && semanticMode === 'fusion' && (
284+ < div className = "space-y-2" >
285+ < Label > { formatMessage ( { id : 'codexlens.search.fusionStrategy' } ) } </ Label >
286+ < Select value = { fusionStrategy } onValueChange = { handleFusionStrategyChange } >
287+ < SelectTrigger >
288+ < SelectValue />
289+ </ SelectTrigger >
290+ < SelectContent >
291+ < SelectItem value = "rrf" >
292+ { formatMessage ( { id : 'codexlens.search.fusionStrategy.rrf' } ) }
293+ </ SelectItem >
294+ < SelectItem value = "dense_rerank" >
295+ { formatMessage ( { id : 'codexlens.search.fusionStrategy.dense_rerank' } ) }
296+ </ SelectItem >
297+ < SelectItem value = "binary" >
298+ { formatMessage ( { id : 'codexlens.search.fusionStrategy.binary' } ) }
299+ </ SelectItem >
300+ < SelectItem value = "hybrid" >
301+ { formatMessage ( { id : 'codexlens.search.fusionStrategy.hybrid' } ) }
302+ </ SelectItem >
303+ < SelectItem value = "staged" >
304+ { formatMessage ( { id : 'codexlens.search.fusionStrategy.staged' } ) }
305+ </ SelectItem >
306+ </ SelectContent >
307+ </ Select >
308+ </ div >
309+ ) }
310+
174311 { /* Query Input */ }
175312 < div className = "space-y-2" >
176313 < Label htmlFor = "search-query" > { formatMessage ( { id : 'codexlens.search.query' } ) } </ Label >
@@ -205,21 +342,7 @@ export function SearchTab({ enabled }: SearchTabProps) {
205342 { formatMessage ( { id : 'codexlens.search.results' } ) }
206343 </ h3 >
207344 < span className = "text-xs text-muted-foreground" >
208- { searchType === 'symbol'
209- ? ( symbolSearch . data ?. success
210- ? `${ symbolSearch . data . symbols ?. length ?? 0 } ${ formatMessage ( { id : 'codexlens.search.resultsCount' } ) } `
211- : ''
212- )
213- : searchType === 'search'
214- ? ( contentSearch . data ?. success
215- ? `${ contentSearch . data . results ?. length ?? 0 } ${ formatMessage ( { id : 'codexlens.search.resultsCount' } ) } `
216- : ''
217- )
218- : ( fileSearch . data ?. success
219- ? `${ fileSearch . data . results ?. length ?? 0 } ${ formatMessage ( { id : 'codexlens.search.resultsCount' } ) } `
220- : ''
221- )
222- }
345+ { getResultCount ( ) }
223346 </ span >
224347 </ div >
225348
@@ -255,7 +378,7 @@ export function SearchTab({ enabled }: SearchTabProps) {
255378 fileSearch . data . success ? (
256379 < div className = "rounded-lg border bg-muted/50 p-4" >
257380 < pre className = "text-xs overflow-auto max-h-96" >
258- { JSON . stringify ( fileSearch . data . results , null , 2 ) }
381+ { JSON . stringify ( fileSearch . data . files , null , 2 ) }
259382 </ pre >
260383 </ div >
261384 ) : (
@@ -264,6 +387,20 @@ export function SearchTab({ enabled }: SearchTabProps) {
264387 </ div >
265388 )
266389 ) }
390+
391+ { searchType === 'semantic' && semanticSearch . data && (
392+ semanticSearch . data . success ? (
393+ < div className = "rounded-lg border bg-muted/50 p-4" >
394+ < pre className = "text-xs overflow-auto max-h-96" >
395+ { JSON . stringify ( semanticSearch . data . results , null , 2 ) }
396+ </ pre >
397+ </ div >
398+ ) : (
399+ < div className = "text-sm text-destructive" >
400+ { semanticSearch . data . error || formatMessage ( { id : 'common.error' } ) }
401+ </ div >
402+ )
403+ ) }
267404 </ div >
268405 ) }
269406 </ div >
0 commit comments