@@ -12,7 +12,7 @@ import { cn } from "@/lib/utils"
1212import { useExtensionState } from "@/context/ExtensionStateContext"
1313import { useAppTranslation } from "@/i18n/TranslationContext"
1414import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
15- import { Popover , PopoverContent , PopoverTrigger , StandardTooltip } from "@/components/ui"
15+ import { Popover , PopoverContent , PopoverTrigger , StandardTooltip , Button } from "@/components/ui"
1616
1717import { IconButton } from "./IconButton"
1818
@@ -45,7 +45,14 @@ export const ModeSelector = ({
4545 const [ searchValue , setSearchValue ] = React . useState ( "" )
4646 const searchInputRef = React . useRef < HTMLInputElement > ( null )
4747 const portalContainer = useRooPortal ( "roo-portal" )
48- const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState ( )
48+ const {
49+ hasOpenedModeSelector,
50+ setHasOpenedModeSelector,
51+ modeSortingMode,
52+ pinnedModes,
53+ togglePinnedMode,
54+ customModeOrder,
55+ } = useExtensionState ( )
4956 const { t } = useAppTranslation ( )
5057
5158 const trackModeSelectorOpened = React . useCallback ( ( ) => {
@@ -99,9 +106,44 @@ export const ModeSelector = ({
99106 [ descriptionSearchItems ] ,
100107 )
101108
109+ // Sort modes based on sorting preferences
110+ const sortedModes = React . useMemo ( ( ) => {
111+ let sorted = [ ...modes ]
112+
113+ if ( modeSortingMode === "manual" && customModeOrder && customModeOrder . length > 0 ) {
114+ // Sort based on custom order
115+ sorted . sort ( ( a , b ) => {
116+ const aIndex = customModeOrder . indexOf ( a . slug )
117+ const bIndex = customModeOrder . indexOf ( b . slug )
118+
119+ // If both are in custom order, sort by their position
120+ if ( aIndex !== - 1 && bIndex !== - 1 ) {
121+ return aIndex - bIndex
122+ }
123+ // If only one is in custom order, it comes first
124+ if ( aIndex !== - 1 ) return - 1
125+ if ( bIndex !== - 1 ) return 1
126+ // Otherwise maintain original order
127+ return 0
128+ } )
129+ } else {
130+ // Alphabetical sorting (default)
131+ sorted . sort ( ( a , b ) => a . name . localeCompare ( b . name ) )
132+ }
133+
134+ // Apply pinning - pinned modes come first
135+ if ( pinnedModes ) {
136+ const pinned = sorted . filter ( ( mode ) => pinnedModes [ mode . slug ] )
137+ const unpinned = sorted . filter ( ( mode ) => ! pinnedModes [ mode . slug ] )
138+ sorted = [ ...pinned , ...unpinned ]
139+ }
140+
141+ return sorted
142+ } , [ modes , modeSortingMode , pinnedModes , customModeOrder ] )
143+
102144 // Filter modes based on search value using fuzzy search with priority.
103145 const filteredModes = React . useMemo ( ( ) => {
104- if ( ! searchValue ) return modes
146+ if ( ! searchValue ) return sortedModes
105147
106148 // First search in names/slugs.
107149 const nameMatches = nameFzfInstance . find ( searchValue )
@@ -118,8 +160,13 @@ export const ModeSelector = ({
118160 . map ( ( result ) => result . item . original ) ,
119161 ]
120162
121- return combinedResults
122- } , [ modes , searchValue , nameFzfInstance , descriptionFzfInstance ] )
163+ // Preserve the sorting order after filtering
164+ const sortedFilteredResults = sortedModes . filter ( ( mode ) =>
165+ combinedResults . some ( ( result ) => result . slug === mode . slug ) ,
166+ )
167+
168+ return sortedFilteredResults
169+ } , [ sortedModes , searchValue , nameFzfInstance , descriptionFzfInstance ] )
123170
124171 const onClearSearch = React . useCallback ( ( ) => {
125172 setSearchValue ( "" )
@@ -230,29 +277,24 @@ export const ModeSelector = ({
230277 </ div >
231278 ) : (
232279 < div className = "py-1" >
233- { filteredModes . map ( ( mode ) => (
234- < div
235- key = { mode . slug }
236- onClick = { ( ) => handleSelect ( mode . slug ) }
237- className = { cn (
238- "px-3 py-1.5 text-sm cursor-pointer flex items-center" ,
239- "hover:bg-vscode-list-hoverBackground" ,
240- mode . slug === value
241- ? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
242- : "" ,
243- ) }
244- data-testid = "mode-selector-item" >
245- < div className = "flex-1 min-w-0" >
246- < div className = "font-bold truncate" > { mode . name } </ div >
247- { mode . description && (
248- < div className = "text-xs text-vscode-descriptionForeground truncate" >
249- { mode . description }
250- </ div >
280+ { /* Show pinned modes first if any */ }
281+ { pinnedModes &&
282+ Object . keys ( pinnedModes ) . length > 0 &&
283+ filteredModes . filter ( ( mode ) => pinnedModes [ mode . slug ] ) . length > 0 && (
284+ < >
285+ { filteredModes
286+ . filter ( ( mode ) => pinnedModes [ mode . slug ] )
287+ . map ( ( mode ) => renderModeItem ( mode , true ) ) }
288+ { /* Separator between pinned and unpinned */ }
289+ { filteredModes . filter ( ( mode ) => ! pinnedModes [ mode . slug ] ) . length > 0 && (
290+ < div className = "mx-1 my-1 h-px bg-vscode-dropdown-foreground/10" />
251291 ) }
252- </ div >
253- { mode . slug === value && < Check className = "ml-auto size-4 p-0.5" /> }
254- </ div >
255- ) ) }
292+ </ >
293+ ) }
294+ { /* Show unpinned modes */ }
295+ { filteredModes
296+ . filter ( ( mode ) => ! pinnedModes || ! pinnedModes [ mode . slug ] )
297+ . map ( ( mode ) => renderModeItem ( mode , false ) ) }
256298 </ div >
257299 ) }
258300 </ div >
@@ -301,4 +343,53 @@ export const ModeSelector = ({
301343 </ PopoverContent >
302344 </ Popover >
303345 )
346+
347+ function renderModeItem ( mode : ( typeof modes ) [ 0 ] , isPinned : boolean ) {
348+ const isSelected = mode . slug === value
349+
350+ return (
351+ < div
352+ key = { mode . slug }
353+ onClick = { ( ) => handleSelect ( mode . slug ) }
354+ className = { cn (
355+ "px-3 py-1.5 text-sm cursor-pointer flex items-center group" ,
356+ "hover:bg-vscode-list-hoverBackground" ,
357+ isSelected
358+ ? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
359+ : "" ,
360+ ) }
361+ data-testid = "mode-selector-item" >
362+ < div className = "flex-1 min-w-0" >
363+ < div className = "font-bold truncate" > { mode . name } </ div >
364+ { mode . description && (
365+ < div className = "text-xs text-vscode-descriptionForeground truncate" > { mode . description } </ div >
366+ ) }
367+ </ div >
368+ < div className = "flex items-center gap-1" >
369+ { isSelected && (
370+ < div className = "size-5 p-1 flex items-center justify-center" >
371+ < Check className = "size-3" />
372+ </ div >
373+ ) }
374+ < StandardTooltip content = { isPinned ? t ( "chat:unpin" ) : t ( "chat:pin" ) } >
375+ < Button
376+ variant = "ghost"
377+ size = "icon"
378+ tabIndex = { - 1 }
379+ onClick = { ( e ) => {
380+ e . stopPropagation ( )
381+ togglePinnedMode ?.( mode . slug )
382+ vscode . postMessage ( { type : "toggleModePin" , text : mode . slug } )
383+ } }
384+ className = { cn ( "size-5 flex items-center justify-center" , {
385+ "opacity-0 group-hover:opacity-100" : ! isPinned && ! isSelected ,
386+ "bg-accent opacity-100" : isPinned ,
387+ } ) } >
388+ < span className = "codicon codicon-pin text-xs opacity-50" />
389+ </ Button >
390+ </ StandardTooltip >
391+ </ div >
392+ </ div >
393+ )
394+ }
304395}
0 commit comments