1- import React from "react"
2- import { ChevronUp , Check } from "lucide-react"
1+ import React , { useState , useRef , useCallback } from "react"
2+ import { ChevronUp , Check , X } from "lucide-react"
33import { cn } from "@/lib/utils"
44import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
5- import { Popover , PopoverContent , PopoverTrigger , StandardTooltip } from "@/components/ui"
5+ import {
6+ Popover ,
7+ PopoverContent ,
8+ PopoverTrigger ,
9+ StandardTooltip ,
10+ Command ,
11+ CommandInput ,
12+ CommandList ,
13+ CommandEmpty ,
14+ CommandItem ,
15+ CommandGroup ,
16+ } from "@/components/ui"
617import { IconButton } from "./IconButton"
718import { vscode } from "@/utils/vscode"
819import { useExtensionState } from "@/context/ExtensionStateContext"
@@ -33,7 +44,9 @@ export const ModeSelector = ({
3344 customModes,
3445 customModePrompts,
3546} : ModeSelectorProps ) => {
36- const [ open , setOpen ] = React . useState ( false )
47+ const [ open , setOpen ] = useState ( false )
48+ const [ searchValue , setSearchValue ] = useState ( "" )
49+ const searchInputRef = useRef < HTMLInputElement > ( null )
3750 const portalContainer = useRooPortal ( "roo-portal" )
3851 const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState ( )
3952 const { t } = useAppTranslation ( )
@@ -61,6 +74,32 @@ export const ModeSelector = ({
6174 // Find the selected mode
6275 const selectedMode = React . useMemo ( ( ) => modes . find ( ( mode ) => mode . slug === value ) , [ modes , value ] )
6376
77+ // Filter modes based on search
78+ const filteredModes = React . useMemo ( ( ) => {
79+ if ( ! searchValue ) return modes
80+ const searchLower = searchValue . toLowerCase ( )
81+ return modes . filter (
82+ ( mode ) => mode . name . toLowerCase ( ) . includes ( searchLower ) || mode . slug . toLowerCase ( ) . includes ( searchLower ) ,
83+ )
84+ } , [ modes , searchValue ] )
85+
86+ // Handler for clearing search input
87+ const onClearSearch = useCallback ( ( ) => {
88+ setSearchValue ( "" )
89+ searchInputRef . current ?. focus ( )
90+ } , [ ] )
91+
92+ // Handler for mode selection
93+ const handleModeSelect = useCallback (
94+ ( modeSlug : string ) => {
95+ onChange ( modeSlug as Mode )
96+ setOpen ( false )
97+ // Clear search after selection
98+ setTimeout ( ( ) => setSearchValue ( "" ) , 100 )
99+ } ,
100+ [ onChange ] ,
101+ )
102+
64103 const trigger = (
65104 < PopoverTrigger
66105 disabled = { disabled }
@@ -88,6 +127,10 @@ export const ModeSelector = ({
88127 onOpenChange = { ( isOpen ) => {
89128 if ( isOpen ) trackModeSelectorOpened ( )
90129 setOpen ( isOpen )
130+ // Clear search when closing
131+ if ( ! isOpen ) {
132+ setTimeout ( ( ) => setSearchValue ( "" ) , 100 )
133+ }
91134 } }
92135 data-testid = "mode-selector-root" >
93136 { title ? < StandardTooltip content = { title } > { trigger } </ StandardTooltip > : trigger }
@@ -97,81 +140,116 @@ export const ModeSelector = ({
97140 sideOffset = { 4 }
98141 container = { portalContainer }
99142 className = "p-0 overflow-hidden min-w-80 max-w-9/10" >
100- < div className = "flex flex-col w-full" >
101- < div className = "p-3 border-b border-vscode-dropdown-border cursor-default" >
102- < div className = "flex flex-row items-center gap-1 p-0 mt-0 mb-1 w-full" >
103- < h4 className = "m-0 pb-2 flex-1" > { t ( "chat:modeSelector.title" ) } </ h4 >
104- < div className = "flex flex-row gap-1 ml-auto mb-1" >
105- < IconButton
106- iconClass = "codicon-extensions"
107- title = { t ( "chat:modeSelector.marketplace" ) }
108- onClick = { ( ) => {
109- window . postMessage (
110- {
111- type : "action" ,
112- action : "marketplaceButtonClicked" ,
113- values : { marketplaceTab : "mode" } ,
114- } ,
115- "*" ,
116- )
117-
118- setOpen ( false )
119- } }
120- />
121- < IconButton
122- iconClass = "codicon-settings-gear"
123- title = { t ( "chat:modeSelector.settings" ) }
124- onClick = { ( ) => {
125- vscode . postMessage ( {
126- type : "switchTab" ,
127- tab : "modes" ,
128- } )
129- setOpen ( false )
130- } }
131- />
132- </ div >
143+ < Command className = "flex flex-col h-full" >
144+ { /* Header with title and info icon */ }
145+ < div className = "p-3 border-b border-vscode-dropdown-border" >
146+ < div className = "flex items-center justify-between mb-2" >
147+ < h4 className = "m-0" > { t ( "chat:modeSelector.title" ) } </ h4 >
148+ < StandardTooltip
149+ content = {
150+ < div >
151+ { t ( "chat:modeSelector.description" ) }
152+ < br />
153+ { modeShortcutText }
154+ </ div >
155+ }
156+ side = "left"
157+ maxWidth = { 300 } >
158+ < span className = "codicon codicon-info text-vscode-descriptionForeground cursor-help" />
159+ </ StandardTooltip >
160+ </ div >
161+
162+ { /* Search input */ }
163+ < div className = "relative" >
164+ < CommandInput
165+ ref = { searchInputRef }
166+ value = { searchValue }
167+ onValueChange = { setSearchValue }
168+ placeholder = { t ( "chat:modeSelector.searchPlaceholder" ) }
169+ className = "h-9 pr-8"
170+ data-testid = "mode-search-input"
171+ />
172+ { searchValue . length > 0 && (
173+ < div className = "absolute right-2 top-0 bottom-0 flex items-center justify-center" >
174+ < X
175+ className = "text-vscode-input-foreground opacity-50 hover:opacity-100 size-4 p-0.5 cursor-pointer"
176+ onClick = { onClearSearch }
177+ />
178+ </ div >
179+ ) }
133180 </ div >
134- < p className = "my-0 pr-4 text-sm w-full" >
135- { t ( "chat:modeSelector.description" ) }
136- < br />
137- { modeShortcutText }
138- </ p >
139181 </ div >
140182
141183 { /* Mode List */ }
142- < div className = "max-h-[400px] overflow-y-auto py-0" >
143- { modes . map ( ( mode ) => (
144- < div
145- className = { cn (
146- "p-2 text-sm cursor-pointer flex flex-row gap-4 items-center" ,
147- "hover:bg-vscode-list-hoverBackground" ,
148- mode . slug === value
149- ? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
150- : "" ,
151- ) }
152- key = { mode . slug }
153- onClick = { ( ) => {
154- onChange ( mode . slug as Mode )
155- setOpen ( false )
156- } }
157- data-testid = "mode-selector-item" >
158- < div className = "flex-grow" >
159- < p className = "m-0 mb-0 font-bold" > { mode . name } </ p >
160- { mode . description && (
161- < p className = "m-0 py-0 pl-4 h-4 flex-1 text-xs overflow-hidden" >
162- { mode . description }
163- </ p >
164- ) }
184+ < CommandList className = "flex-1 overflow-y-auto" >
185+ < CommandEmpty >
186+ { searchValue && (
187+ < div className = "py-4 px-3 text-sm text-vscode-descriptionForeground text-center" >
188+ { t ( "chat:modeSelector.noMatchFound" ) }
165189 </ div >
166- { mode . slug === value ? (
167- < Check className = "m-0 size-4 p-0.5" />
168- ) : (
169- < div className = "size-4" />
170- ) }
171- </ div >
172- ) ) }
190+ ) }
191+ </ CommandEmpty >
192+ < CommandGroup >
193+ { filteredModes . map ( ( mode ) => (
194+ < CommandItem
195+ key = { mode . slug }
196+ value = { mode . slug }
197+ onSelect = { ( ) => handleModeSelect ( mode . slug ) }
198+ className = { cn (
199+ "p-2 cursor-pointer" ,
200+ mode . slug === value && "bg-vscode-list-activeSelectionBackground" ,
201+ ) }
202+ data-testid = "mode-selector-item" >
203+ < div className = "flex items-center gap-4 w-full" >
204+ < div className = "flex-grow min-w-0" >
205+ < p className = "m-0 font-bold" > { mode . name } </ p >
206+ { mode . description && (
207+ < p className = "m-0 mt-0.5 text-xs text-vscode-descriptionForeground truncate" >
208+ { mode . description }
209+ </ p >
210+ ) }
211+ </ div >
212+ { mode . slug === value ? (
213+ < Check className = "size-4 flex-shrink-0" />
214+ ) : (
215+ < div className = "size-4 flex-shrink-0" />
216+ ) }
217+ </ div >
218+ </ CommandItem >
219+ ) ) }
220+ </ CommandGroup >
221+ </ CommandList >
222+
223+ { /* Footer with marketplace and settings buttons */ }
224+ < div className = "p-3 border-t border-vscode-dropdown-border flex justify-end gap-2" >
225+ < IconButton
226+ iconClass = "codicon-extensions"
227+ title = { t ( "chat:modeSelector.marketplace" ) }
228+ onClick = { ( ) => {
229+ window . postMessage (
230+ {
231+ type : "action" ,
232+ action : "marketplaceButtonClicked" ,
233+ values : { marketplaceTab : "mode" } ,
234+ } ,
235+ "*" ,
236+ )
237+ setOpen ( false )
238+ } }
239+ />
240+ < IconButton
241+ iconClass = "codicon-settings-gear"
242+ title = { t ( "chat:modeSelector.settings" ) }
243+ onClick = { ( ) => {
244+ vscode . postMessage ( {
245+ type : "switchTab" ,
246+ tab : "modes" ,
247+ } )
248+ setOpen ( false )
249+ } }
250+ />
173251 </ div >
174- </ div >
252+ </ Command >
175253 </ PopoverContent >
176254 </ Popover >
177255 )
0 commit comments