11import React from "react"
2- import { ChevronUp , Check } from "lucide-react"
2+ import { ChevronUp , Check , Info , 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+ CommandEmpty ,
12+ CommandGroup ,
13+ CommandInput ,
14+ CommandItem ,
15+ CommandList ,
16+ } from "@/components/ui"
617import { IconButton } from "./IconButton"
718import { vscode } from "@/utils/vscode"
819import { useExtensionState } from "@/context/ExtensionStateContext"
@@ -34,6 +45,8 @@ export const ModeSelector = ({
3445 customModePrompts,
3546} : ModeSelectorProps ) => {
3647 const [ open , setOpen ] = React . useState ( false )
48+ const [ searchValue , setSearchValue ] = React . useState ( "" )
49+ const searchInputRef = React . useRef < HTMLInputElement > ( null )
3750 const portalContainer = useRooPortal ( "roo-portal" )
3851 const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState ( )
3952 const { t } = useAppTranslation ( )
@@ -61,6 +74,30 @@ 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 ) =>
83+ mode . name . toLowerCase ( ) . includes ( searchLower ) || mode . description ?. toLowerCase ( ) . includes ( searchLower ) ,
84+ )
85+ } , [ modes , searchValue ] )
86+
87+ const onClearSearch = React . useCallback ( ( ) => {
88+ setSearchValue ( "" )
89+ searchInputRef . current ?. focus ( )
90+ } , [ ] )
91+
92+ const handleModeSelect = React . useCallback (
93+ ( modeSlug : string ) => {
94+ onChange ( modeSlug as Mode )
95+ setOpen ( false )
96+ setSearchValue ( "" )
97+ } ,
98+ [ onChange ] ,
99+ )
100+
64101 const trigger = (
65102 < PopoverTrigger
66103 disabled = { disabled }
@@ -86,7 +123,12 @@ export const ModeSelector = ({
86123 < Popover
87124 open = { open }
88125 onOpenChange = { ( isOpen ) => {
89- if ( isOpen ) trackModeSelectorOpened ( )
126+ if ( isOpen ) {
127+ trackModeSelectorOpened ( )
128+ } else {
129+ // Clear search when closing
130+ setSearchValue ( "" )
131+ }
90132 setOpen ( isOpen )
91133 } }
92134 data-testid = "mode-selector-root" >
@@ -97,81 +139,115 @@ export const ModeSelector = ({
97139 sideOffset = { 4 }
98140 container = { portalContainer }
99141 className = "p-0 overflow-hidden min-w-80 max-w-9/10" >
100- < div className = "flex flex-col w-full" >
142+ < Command className = "flex flex-col w-full" >
143+ { /* Header with title and description */ }
101144 < div className = "p-3 border-b border-vscode-dropdown-border cursor-default" >
102145 < div className = "flex flex-row items-center gap-1 p-0 mt-0 mb-1 w-full" >
103146 < 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- } }
147+ < StandardTooltip
148+ content = {
149+ < div className = "max-w-xs" >
150+ { t ( "chat:modeSelector.description" ) }
151+ < br />
152+ < br />
153+ { modeShortcutText }
154+ </ div >
155+ } >
156+ < Info className = "size-4 opacity-60 hover:opacity-100 cursor-help mb-2" />
157+ </ StandardTooltip >
158+ </ div >
159+ </ div >
160+
161+ { /* Search Input */ }
162+ < div className = "relative" >
163+ < CommandInput
164+ ref = { searchInputRef }
165+ value = { searchValue }
166+ onValueChange = { setSearchValue }
167+ placeholder = { t ( "chat:modeSelector.searchPlaceholder" , { defaultValue : "Search modes..." } ) }
168+ className = "h-9 mr-4"
169+ data-testid = "mode-search-input"
170+ />
171+ { searchValue . length > 0 && (
172+ < div className = "absolute right-2 top-0 bottom-0 flex items-center justify-center" >
173+ < X
174+ className = "text-vscode-input-foreground opacity-50 hover:opacity-100 size-4 p-0.5 cursor-pointer"
175+ onClick = { onClearSearch }
131176 />
132177 </ div >
133- </ div >
134- < p className = "my-0 pr-4 text-sm w-full" >
135- { t ( "chat:modeSelector.description" ) }
136- < br />
137- { modeShortcutText }
138- </ p >
178+ ) }
139179 </ div >
140180
141181 { /* 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- ) }
182+ < CommandList className = "max-h-[300px]" >
183+ < CommandEmpty >
184+ { searchValue && (
185+ < div className = "py-2 px-1 text-sm" >
186+ { t ( "chat:modeSelector.noMatchFound" , { defaultValue : "No modes found" } ) }
165187 </ 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- ) ) }
188+ ) }
189+ </ CommandEmpty >
190+ < CommandGroup >
191+ { filteredModes . map ( ( mode ) => (
192+ < CommandItem
193+ key = { mode . slug }
194+ value = { mode . slug }
195+ onSelect = { handleModeSelect }
196+ data-testid = "mode-selector-item"
197+ className = { cn (
198+ "flex flex-row gap-4 items-center" ,
199+ mode . slug === value
200+ ? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
201+ : "" ,
202+ ) } >
203+ < div className = "flex-grow" >
204+ < p className = "m-0 mb-0 font-bold" > { mode . name } </ p >
205+ { mode . description && (
206+ < p className = "m-0 py-0 pl-4 h-4 flex-1 text-xs overflow-hidden" >
207+ { mode . description }
208+ </ p >
209+ ) }
210+ </ div >
211+ { mode . slug === value ? (
212+ < Check className = "m-0 size-4 p-0.5" />
213+ ) : (
214+ < div className = "size-4" />
215+ ) }
216+ </ CommandItem >
217+ ) ) }
218+ </ CommandGroup >
219+ </ CommandList >
220+
221+ { /* Bottom section with marketplace and settings buttons */ }
222+ < div className = "p-2 border-t border-vscode-dropdown-border flex flex-row gap-1 justify-end" >
223+ < IconButton
224+ iconClass = "codicon-extensions"
225+ title = { t ( "chat:modeSelector.marketplace" ) }
226+ onClick = { ( ) => {
227+ window . postMessage (
228+ {
229+ type : "action" ,
230+ action : "marketplaceButtonClicked" ,
231+ values : { marketplaceTab : "mode" } ,
232+ } ,
233+ "*" ,
234+ )
235+ setOpen ( false )
236+ } }
237+ />
238+ < IconButton
239+ iconClass = "codicon-settings-gear"
240+ title = { t ( "chat:modeSelector.settings" ) }
241+ onClick = { ( ) => {
242+ vscode . postMessage ( {
243+ type : "switchTab" ,
244+ tab : "modes" ,
245+ } )
246+ setOpen ( false )
247+ } }
248+ />
173249 </ div >
174- </ div >
250+ </ Command >
175251 </ PopoverContent >
176252 </ Popover >
177253 )
0 commit comments