Skip to content

Commit 0482256

Browse files
committed
feat: add search functionality to mode selector and reorganize layout
- Add search input with clear button to filter modes - Move marketplace and settings buttons to bottom of popup - Replace instruction text with info icon tooltip - Update all localization files with new translation keys - Maintain consistent UI pattern with model selector Fixes #6128
1 parent 4042fb0 commit 0482256

File tree

19 files changed

+205
-91
lines changed

19 files changed

+205
-91
lines changed

webview-ui/src/components/chat/ModeSelector.tsx

Lines changed: 151 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
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"
33
import { cn } from "@/lib/utils"
44
import { 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"
617
import { IconButton } from "./IconButton"
718
import { vscode } from "@/utils/vscode"
819
import { 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
)

webview-ui/src/i18n/locales/ca/chat.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/de/chat.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/en/chat.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@
118118
"title": "Modes",
119119
"marketplace": "Mode Marketplace",
120120
"settings": "Mode Settings",
121-
"description": "Specialized personas that tailor Roo's behavior."
121+
"description": "Specialized personas that tailor Roo's behavior.",
122+
"searchPlaceholder": "Search modes...",
123+
"noMatchFound": "No modes found"
122124
},
123125
"enhancePromptDescription": "The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works.",
124126
"addImages": "Add images to message",

webview-ui/src/i18n/locales/es/chat.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/fr/chat.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/hi/chat.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/id/chat.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/it/chat.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/ja/chat.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)