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
This commit is contained in:
Roo Code 2025-07-23 17:15:00 +00:00
parent 4042fb0fd0
commit 0482256a6e
19 changed files with 205 additions and 91 deletions

View file

@ -1,8 +1,19 @@
import React from "react"
import { ChevronUp, Check } from "lucide-react"
import React, { useState, useRef, useCallback } from "react"
import { ChevronUp, Check, X } from "lucide-react"
import { cn } from "@/lib/utils"
import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
import {
Popover,
PopoverContent,
PopoverTrigger,
StandardTooltip,
Command,
CommandInput,
CommandList,
CommandEmpty,
CommandItem,
CommandGroup,
} from "@/components/ui"
import { IconButton } from "./IconButton"
import { vscode } from "@/utils/vscode"
import { useExtensionState } from "@/context/ExtensionStateContext"
@ -33,7 +44,9 @@ export const ModeSelector = ({
customModes,
customModePrompts,
}: ModeSelectorProps) => {
const [open, setOpen] = React.useState(false)
const [open, setOpen] = useState(false)
const [searchValue, setSearchValue] = useState("")
const searchInputRef = useRef<HTMLInputElement>(null)
const portalContainer = useRooPortal("roo-portal")
const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState()
const { t } = useAppTranslation()
@ -61,6 +74,32 @@ export const ModeSelector = ({
// Find the selected mode
const selectedMode = React.useMemo(() => modes.find((mode) => mode.slug === value), [modes, value])
// Filter modes based on search
const filteredModes = React.useMemo(() => {
if (!searchValue) return modes
const searchLower = searchValue.toLowerCase()
return modes.filter(
(mode) => mode.name.toLowerCase().includes(searchLower) || mode.slug.toLowerCase().includes(searchLower),
)
}, [modes, searchValue])
// Handler for clearing search input
const onClearSearch = useCallback(() => {
setSearchValue("")
searchInputRef.current?.focus()
}, [])
// Handler for mode selection
const handleModeSelect = useCallback(
(modeSlug: string) => {
onChange(modeSlug as Mode)
setOpen(false)
// Clear search after selection
setTimeout(() => setSearchValue(""), 100)
},
[onChange],
)
const trigger = (
<PopoverTrigger
disabled={disabled}
@ -88,6 +127,10 @@ export const ModeSelector = ({
onOpenChange={(isOpen) => {
if (isOpen) trackModeSelectorOpened()
setOpen(isOpen)
// Clear search when closing
if (!isOpen) {
setTimeout(() => setSearchValue(""), 100)
}
}}
data-testid="mode-selector-root">
{title ? <StandardTooltip content={title}>{trigger}</StandardTooltip> : trigger}
@ -97,81 +140,116 @@ export const ModeSelector = ({
sideOffset={4}
container={portalContainer}
className="p-0 overflow-hidden min-w-80 max-w-9/10">
<div className="flex flex-col w-full">
<div className="p-3 border-b border-vscode-dropdown-border cursor-default">
<div className="flex flex-row items-center gap-1 p-0 mt-0 mb-1 w-full">
<h4 className="m-0 pb-2 flex-1">{t("chat:modeSelector.title")}</h4>
<div className="flex flex-row gap-1 ml-auto mb-1">
<IconButton
iconClass="codicon-extensions"
title={t("chat:modeSelector.marketplace")}
onClick={() => {
window.postMessage(
{
type: "action",
action: "marketplaceButtonClicked",
values: { marketplaceTab: "mode" },
},
"*",
)
setOpen(false)
}}
/>
<IconButton
iconClass="codicon-settings-gear"
title={t("chat:modeSelector.settings")}
onClick={() => {
vscode.postMessage({
type: "switchTab",
tab: "modes",
})
setOpen(false)
}}
/>
</div>
<Command className="flex flex-col h-full">
{/* Header with title and info icon */}
<div className="p-3 border-b border-vscode-dropdown-border">
<div className="flex items-center justify-between mb-2">
<h4 className="m-0">{t("chat:modeSelector.title")}</h4>
<StandardTooltip
content={
<div>
{t("chat:modeSelector.description")}
<br />
{modeShortcutText}
</div>
}
side="left"
maxWidth={300}>
<span className="codicon codicon-info text-vscode-descriptionForeground cursor-help" />
</StandardTooltip>
</div>
{/* Search input */}
<div className="relative">
<CommandInput
ref={searchInputRef}
value={searchValue}
onValueChange={setSearchValue}
placeholder={t("chat:modeSelector.searchPlaceholder")}
className="h-9 pr-8"
data-testid="mode-search-input"
/>
{searchValue.length > 0 && (
<div className="absolute right-2 top-0 bottom-0 flex items-center justify-center">
<X
className="text-vscode-input-foreground opacity-50 hover:opacity-100 size-4 p-0.5 cursor-pointer"
onClick={onClearSearch}
/>
</div>
)}
</div>
<p className="my-0 pr-4 text-sm w-full">
{t("chat:modeSelector.description")}
<br />
{modeShortcutText}
</p>
</div>
{/* Mode List */}
<div className="max-h-[400px] overflow-y-auto py-0">
{modes.map((mode) => (
<div
className={cn(
"p-2 text-sm cursor-pointer flex flex-row gap-4 items-center",
"hover:bg-vscode-list-hoverBackground",
mode.slug === value
? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
: "",
)}
key={mode.slug}
onClick={() => {
onChange(mode.slug as Mode)
setOpen(false)
}}
data-testid="mode-selector-item">
<div className="flex-grow">
<p className="m-0 mb-0 font-bold">{mode.name}</p>
{mode.description && (
<p className="m-0 py-0 pl-4 h-4 flex-1 text-xs overflow-hidden">
{mode.description}
</p>
)}
<CommandList className="flex-1 overflow-y-auto">
<CommandEmpty>
{searchValue && (
<div className="py-4 px-3 text-sm text-vscode-descriptionForeground text-center">
{t("chat:modeSelector.noMatchFound")}
</div>
{mode.slug === value ? (
<Check className="m-0 size-4 p-0.5" />
) : (
<div className="size-4" />
)}
</div>
))}
)}
</CommandEmpty>
<CommandGroup>
{filteredModes.map((mode) => (
<CommandItem
key={mode.slug}
value={mode.slug}
onSelect={() => handleModeSelect(mode.slug)}
className={cn(
"p-2 cursor-pointer",
mode.slug === value && "bg-vscode-list-activeSelectionBackground",
)}
data-testid="mode-selector-item">
<div className="flex items-center gap-4 w-full">
<div className="flex-grow min-w-0">
<p className="m-0 font-bold">{mode.name}</p>
{mode.description && (
<p className="m-0 mt-0.5 text-xs text-vscode-descriptionForeground truncate">
{mode.description}
</p>
)}
</div>
{mode.slug === value ? (
<Check className="size-4 flex-shrink-0" />
) : (
<div className="size-4 flex-shrink-0" />
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
{/* Footer with marketplace and settings buttons */}
<div className="p-3 border-t border-vscode-dropdown-border flex justify-end gap-2">
<IconButton
iconClass="codicon-extensions"
title={t("chat:modeSelector.marketplace")}
onClick={() => {
window.postMessage(
{
type: "action",
action: "marketplaceButtonClicked",
values: { marketplaceTab: "mode" },
},
"*",
)
setOpen(false)
}}
/>
<IconButton
iconClass="codicon-settings-gear"
title={t("chat:modeSelector.settings")}
onClick={() => {
vscode.postMessage({
type: "switchTab",
tab: "modes",
})
setOpen(false)
}}
/>
</div>
</div>
</Command>
</PopoverContent>
</Popover>
)

View file

@ -116,7 +116,9 @@
"title": "Modes",
"marketplace": "Marketplace de Modes",
"settings": "Configuració de Modes",
"description": "Personalitats especialitzades que adapten el comportament de Roo."
"description": "Personalitats especialitzades que adapten el comportament de Roo.",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"errorReadingFile": "Error en llegir el fitxer:",
"noValidImages": "No s'ha processat cap imatge vàlida",

View file

@ -116,7 +116,9 @@
"title": "Modi",
"marketplace": "Modus-Marketplace",
"settings": "Modus-Einstellungen",
"description": "Spezialisierte Personas, die Roos Verhalten anpassen."
"description": "Spezialisierte Personas, die Roos Verhalten anpassen.",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"errorReadingFile": "Fehler beim Lesen der Datei:",
"noValidImages": "Keine gültigen Bilder wurden verarbeitet",

View file

@ -118,7 +118,9 @@
"title": "Modes",
"marketplace": "Mode Marketplace",
"settings": "Mode Settings",
"description": "Specialized personas that tailor Roo's behavior."
"description": "Specialized personas that tailor Roo's behavior.",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"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.",
"addImages": "Add images to message",

View file

@ -116,7 +116,9 @@
"title": "Modos",
"marketplace": "Marketplace de Modos",
"settings": "Configuración de Modos",
"description": "Personalidades especializadas que adaptan el comportamiento de Roo."
"description": "Personalidades especializadas que adaptan el comportamiento de Roo.",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"errorReadingFile": "Error al leer el archivo:",
"noValidImages": "No se procesaron imágenes válidas",

View file

@ -116,7 +116,9 @@
"title": "Modes",
"marketplace": "Marketplace de Modes",
"settings": "Paramètres des Modes",
"description": "Personas spécialisés qui adaptent le comportement de Roo."
"description": "Personas spécialisés qui adaptent le comportement de Roo.",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"errorReadingFile": "Erreur lors de la lecture du fichier :",
"noValidImages": "Aucune image valide n'a été traitée",

View file

@ -116,7 +116,9 @@
"title": "मोड्स",
"marketplace": "मोड मार्केटप्लेस",
"settings": "मोड सेटिंग्स",
"description": "विशेष व्यक्तित्व जो Roo के व्यवहार को अनुकूलित करते हैं।"
"description": "विशेष व्यक्तित्व जो Roo के व्यवहार को अनुकूलित करते हैं।",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"errorReadingFile": "फ़ाइल पढ़ने में त्रुटि:",
"noValidImages": "कोई मान्य चित्र प्रोसेस नहीं किया गया",

View file

@ -122,7 +122,9 @@
"title": "Mode",
"marketplace": "Marketplace Mode",
"settings": "Pengaturan Mode",
"description": "Persona khusus yang menyesuaikan perilaku Roo."
"description": "Persona khusus yang menyesuaikan perilaku Roo.",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"addImages": "Tambahkan gambar ke pesan",
"sendMessage": "Kirim pesan",

View file

@ -116,7 +116,9 @@
"title": "Modalità",
"marketplace": "Marketplace delle Modalità",
"settings": "Impostazioni Modalità",
"description": "Personalità specializzate che adattano il comportamento di Roo."
"description": "Personalità specializzate che adattano il comportamento di Roo.",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"errorReadingFile": "Errore nella lettura del file:",
"noValidImages": "Nessuna immagine valida è stata elaborata",

View file

@ -116,7 +116,9 @@
"title": "モード",
"marketplace": "モードマーケットプレイス",
"settings": "モード設定",
"description": "Rooの動作をカスタマイズする専門的なペルソナ。"
"description": "Rooの動作をカスタマイズする専門的なペルソナ。",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"errorReadingFile": "ファイル読み込みエラー:",
"noValidImages": "有効な画像が処理されませんでした",

View file

@ -116,7 +116,9 @@
"title": "모드",
"marketplace": "모드 마켓플레이스",
"settings": "모드 설정",
"description": "Roo의 행동을 맞춤화하는 전문화된 페르소나."
"description": "Roo의 행동을 맞춤화하는 전문화된 페르소나.",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"errorReadingFile": "파일 읽기 오류:",
"noValidImages": "처리된 유효한 이미지가 없습니다",

View file

@ -108,7 +108,9 @@
"title": "Modi",
"marketplace": "Modus Marktplaats",
"settings": "Modus Instellingen",
"description": "Gespecialiseerde persona's die het gedrag van Roo aanpassen."
"description": "Gespecialiseerde persona's die het gedrag van Roo aanpassen.",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"addImages": "Afbeeldingen toevoegen aan bericht",
"sendMessage": "Bericht verzenden",

View file

@ -116,7 +116,9 @@
"title": "Tryby",
"marketplace": "Marketplace Trybów",
"settings": "Ustawienia Trybów",
"description": "Wyspecjalizowane persony, które dostosowują zachowanie Roo."
"description": "Wyspecjalizowane persony, które dostosowują zachowanie Roo.",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"errorReadingFile": "Błąd odczytu pliku:",
"noValidImages": "Nie przetworzono żadnych prawidłowych obrazów",

View file

@ -116,7 +116,9 @@
"title": "Modos",
"marketplace": "Marketplace de Modos",
"settings": "Configurações de Modos",
"description": "Personas especializadas que adaptam o comportamento do Roo."
"description": "Personas especializadas que adaptam o comportamento do Roo.",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"errorReadingFile": "Erro ao ler arquivo:",
"noValidImages": "Nenhuma imagem válida foi processada",

View file

@ -108,7 +108,9 @@
"title": "Режимы",
"marketplace": "Маркетплейс режимов",
"settings": "Настройки режимов",
"description": "Специализированные персоны, которые настраивают поведение Roo."
"description": "Специализированные персоны, которые настраивают поведение Roo.",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"addImages": "Добавить изображения к сообщению",
"sendMessage": "Отправить сообщение",

View file

@ -116,7 +116,9 @@
"title": "Modlar",
"marketplace": "Mod Pazaryeri",
"settings": "Mod Ayarları",
"description": "Roo'nun davranışını özelleştiren uzmanlaşmış kişilikler."
"description": "Roo'nun davranışını özelleştiren uzmanlaşmış kişilikler.",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"errorReadingFile": "Dosya okuma hatası:",
"noValidImages": "Hiçbir geçerli resim işlenmedi",

View file

@ -116,7 +116,9 @@
"title": "Chế độ",
"marketplace": "Chợ Chế độ",
"settings": "Cài đặt Chế độ",
"description": "Các nhân cách chuyên biệt điều chỉnh hành vi của Roo."
"description": "Các nhân cách chuyên biệt điều chỉnh hành vi của Roo.",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"errorReadingFile": "Lỗi khi đọc tệp:",
"noValidImages": "Không có hình ảnh hợp lệ nào được xử lý",

View file

@ -116,7 +116,9 @@
"title": "模式",
"marketplace": "模式市场",
"settings": "模式设置",
"description": "专门定制Roo行为的角色。"
"description": "专门定制Roo行为的角色。",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"errorReadingFile": "读取文件时出错:",
"noValidImages": "没有处理有效图片",

View file

@ -116,7 +116,9 @@
"title": "模式",
"marketplace": "模式市集",
"settings": "模式設定",
"description": "專門定制Roo行為的角色。"
"description": "專門定制Roo行為的角色。",
"searchPlaceholder": "Search modes...",
"noMatchFound": "No modes found"
},
"errorReadingFile": "讀取檔案時發生錯誤:",
"noValidImages": "未處理到任何有效圖片",