From d40196321b3f6e99baecca649cd6c2f6855cd437 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Wed, 23 Jul 2025 16:22:36 -0600 Subject: [PATCH 1/8] feat: Add search functionality to mode selector popup and reorganize layout - Add fuzzy search functionality with Fzf library for better mode discovery - Move marketplace/settings buttons to bottom of popup for cleaner layout - Convert instruction text to tooltip with info icon for space efficiency - Align search input styling with API configuration selector pattern - Improve spacing consistency throughout the component - Add 'Search modes...' placeholder text for better UX - Fix React Hook dependency arrays and useCallback optimization Resolves #6128 --- .../src/components/chat/ModeSelector.tsx | 222 ++++++++++++------ 1 file changed, 148 insertions(+), 74 deletions(-) diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index 336e9f83578..72ed216e51f 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -1,5 +1,5 @@ import React from "react" -import { ChevronUp, Check } from "lucide-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" @@ -11,6 +11,7 @@ import { Mode, getAllModes } from "@roo/modes" import { ModeConfig, CustomModePrompts } from "@roo-code/types" import { telemetryClient } from "@/utils/TelemetryClient" import { TelemetryEventName } from "@roo-code/types" +import { Fzf } from "fzf" interface ModeSelectorProps { value: Mode @@ -34,11 +35,13 @@ export const ModeSelector = ({ customModePrompts, }: ModeSelectorProps) => { const [open, setOpen] = React.useState(false) + const [searchValue, setSearchValue] = React.useState("") + const searchInputRef = React.useRef(null) const portalContainer = useRooPortal("roo-portal") const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState() const { t } = useAppTranslation() - const trackModeSelectorOpened = () => { + const trackModeSelectorOpened = React.useCallback(() => { // Track telemetry every time the mode selector is opened telemetryClient.capture(TelemetryEventName.MODE_SELECTOR_OPENED) @@ -47,7 +50,7 @@ export const ModeSelector = ({ setHasOpenedModeSelector(true) vscode.postMessage({ type: "hasOpenedModeSelector", bool: true }) } - } + }, [hasOpenedModeSelector, setHasOpenedModeSelector]) // Get all modes including custom modes and merge custom prompt descriptions const modes = React.useMemo(() => { @@ -61,6 +64,59 @@ export const ModeSelector = ({ // Find the selected mode const selectedMode = React.useMemo(() => modes.find((mode) => mode.slug === value), [modes, value]) + // Memoize searchable items for fuzzy search + const searchableItems = React.useMemo(() => { + return modes.map((mode) => ({ + original: mode, + searchStr: [mode.name, mode.slug, mode.description].filter(Boolean).join(" "), + })) + }, [modes]) + + // Create a memoized Fzf instance + const fzfInstance = React.useMemo(() => { + return new Fzf(searchableItems, { + selector: (item) => item.searchStr, + }) + }, [searchableItems]) + + // Filter modes based on search value using fuzzy search + const filteredModes = React.useMemo(() => { + if (!searchValue) return modes + + const matchingItems = fzfInstance.find(searchValue).map((result) => result.item.original) + return matchingItems + }, [modes, searchValue, fzfInstance]) + + const onClearSearch = React.useCallback(() => { + setSearchValue("") + searchInputRef.current?.focus() + }, []) + + const handleSelect = React.useCallback( + (modeSlug: string) => { + onChange(modeSlug as Mode) + setOpen(false) + // Clear search after selection + requestAnimationFrame(() => setSearchValue("")) + }, + [onChange], + ) + + const onOpenChange = React.useCallback( + (isOpen: boolean) => { + if (isOpen) trackModeSelectorOpened() + setOpen(isOpen) + // Clear search when closing + if (!isOpen) { + requestAnimationFrame(() => setSearchValue("")) + } + }, + [trackModeSelectorOpened], + ) + + // Combine instruction text for tooltip + const instructionText = `${t("chat:modeSelector.description")} ${modeShortcutText}` + const trigger = ( { - if (isOpen) trackModeSelectorOpened() - setOpen(isOpen) - }} - data-testid="mode-selector-root"> + {title ? {trigger} : trigger}
-
-
-

{t("chat:modeSelector.title")}

-
- { - window.postMessage( - { - type: "action", - action: "marketplaceButtonClicked", - values: { marketplaceTab: "mode" }, - }, - "*", - ) - - setOpen(false) - }} - /> - { - vscode.postMessage({ - type: "switchTab", - tab: "modes", - }) - setOpen(false) - }} + {/* Search input only */} +
+ setSearchValue(e.target.value)} + placeholder="Search modes..." + className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0" + data-testid="mode-search-input" + /> + {searchValue.length > 0 && ( +
+
-
-

- {t("chat:modeSelector.description")} -
- {modeShortcutText} -

+ )}
{/* Mode List */} -
- {modes.map((mode) => ( -
+ {filteredModes.length === 0 && searchValue ? ( +
+ {t("settings:modelPicker.noMatchFound")} +
+ ) : ( +
+ {filteredModes.map((mode) => ( +
handleSelect(mode.slug)} + className={cn( + "px-3 py-1.5 text-sm cursor-pointer flex items-center", + "hover:bg-vscode-list-hoverBackground", + mode.slug === value + ? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground" + : "", + )} + data-testid="mode-selector-item"> +
+
{mode.name}
+ {mode.description && ( +
+ {mode.description} +
+ )} +
+ {mode.slug === value && } +
+ ))} +
+ )} +
+ + {/* Bottom bar with buttons on left and title on right */} +
+
+ { - onChange(mode.slug as Mode) + window.postMessage( + { + type: "action", + action: "marketplaceButtonClicked", + values: { marketplaceTab: "mode" }, + }, + "*", + ) setOpen(false) }} - data-testid="mode-selector-item"> -
-

{mode.name}

- {mode.description && ( -

- {mode.description} -

- )} -
- {mode.slug === value ? ( - - ) : ( -
- )} -
- ))} + /> + { + vscode.postMessage({ + type: "switchTab", + tab: "modes", + }) + setOpen(false) + }} + /> +
+ + {/* Info icon and title on the right with matching spacing */} +
+ + + +

+ {t("chat:modeSelector.title")} +

+
From 37307d50e1b431f03c705dca79a67186d970af11 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Wed, 23 Jul 2025 17:00:54 -0600 Subject: [PATCH 2/8] fix: address i18n issues in ModeSelector search functionality - Replace hardcoded 'Search modes...' placeholder with t('common:ui.search_placeholder') - Replace settings namespace usage with hardcoded 'No results found' to match SelectDropdown pattern - Ensures proper internationalization and consistent namespace usage --- webview-ui/src/components/chat/ModeSelector.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index 72ed216e51f..69be625d36e 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -155,7 +155,7 @@ export const ModeSelector = ({ ref={searchInputRef} value={searchValue} onChange={(e) => setSearchValue(e.target.value)} - placeholder="Search modes..." + placeholder={t("common:ui.search_placeholder")} className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0" data-testid="mode-search-input" /> @@ -172,9 +172,7 @@ export const ModeSelector = ({ {/* Mode List */}
{filteredModes.length === 0 && searchValue ? ( -
- {t("settings:modelPicker.noMatchFound")} -
+
No results found
) : (
{filteredModes.map((mode) => ( From 45e3606653d2c7fca929267a27d278c593dfad4c Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Wed, 23 Jul 2025 17:19:46 -0600 Subject: [PATCH 3/8] feat: add specific 'Search modes...' translation key for ModeSelector - Add new modeSelector.searchPlaceholder key to all language files - Update ModeSelector to use chat:modeSelector.searchPlaceholder instead of generic search placeholder - Provides more contextual and user-friendly search placeholder text - Maintains consistency across all 18 supported languages Co-authored-by: Roo Code Translate Mode --- webview-ui/src/components/chat/ModeSelector.tsx | 2 +- webview-ui/src/i18n/locales/ca/chat.json | 3 ++- webview-ui/src/i18n/locales/de/chat.json | 3 ++- webview-ui/src/i18n/locales/en/chat.json | 3 ++- webview-ui/src/i18n/locales/es/chat.json | 3 ++- webview-ui/src/i18n/locales/fr/chat.json | 3 ++- webview-ui/src/i18n/locales/hi/chat.json | 3 ++- webview-ui/src/i18n/locales/id/chat.json | 3 ++- webview-ui/src/i18n/locales/it/chat.json | 3 ++- webview-ui/src/i18n/locales/ja/chat.json | 3 ++- webview-ui/src/i18n/locales/ko/chat.json | 3 ++- webview-ui/src/i18n/locales/nl/chat.json | 3 ++- webview-ui/src/i18n/locales/pl/chat.json | 3 ++- webview-ui/src/i18n/locales/pt-BR/chat.json | 3 ++- webview-ui/src/i18n/locales/ru/chat.json | 3 ++- webview-ui/src/i18n/locales/tr/chat.json | 3 ++- webview-ui/src/i18n/locales/vi/chat.json | 3 ++- webview-ui/src/i18n/locales/zh-CN/chat.json | 3 ++- webview-ui/src/i18n/locales/zh-TW/chat.json | 3 ++- 19 files changed, 37 insertions(+), 19 deletions(-) diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index 69be625d36e..00a871f557e 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -155,7 +155,7 @@ export const ModeSelector = ({ ref={searchInputRef} value={searchValue} onChange={(e) => setSearchValue(e.target.value)} - placeholder={t("common:ui.search_placeholder")} + placeholder={t("chat:modeSelector.searchPlaceholder")} className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0" data-testid="mode-search-input" /> diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 01d9ee1c1a8..e8c1d145cc0 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -116,7 +116,8 @@ "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": "Cerca modes..." }, "errorReadingFile": "Error en llegir el fitxer:", "noValidImages": "No s'ha processat cap imatge vàlida", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 032145234d9..277800d4ee3 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -116,7 +116,8 @@ "title": "Modi", "marketplace": "Modus-Marketplace", "settings": "Modus-Einstellungen", - "description": "Spezialisierte Personas, die Roos Verhalten anpassen." + "description": "Spezialisierte Personas, die Roos Verhalten anpassen.", + "searchPlaceholder": "Modi suchen..." }, "errorReadingFile": "Fehler beim Lesen der Datei:", "noValidImages": "Keine gültigen Bilder wurden verarbeitet", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 3bbb3fbf72f..4fd374cfff8 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -118,7 +118,8 @@ "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..." }, "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", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index adcfd1d40cd..5792333b9d9 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -116,7 +116,8 @@ "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": "Buscar modos..." }, "errorReadingFile": "Error al leer el archivo:", "noValidImages": "No se procesaron imágenes válidas", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 3e49a648677..daa02759ea6 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -116,7 +116,8 @@ "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": "Rechercher des modes..." }, "errorReadingFile": "Erreur lors de la lecture du fichier :", "noValidImages": "Aucune image valide n'a été traitée", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 3b5c7b8a67c..eddc9097b79 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -116,7 +116,8 @@ "title": "मोड्स", "marketplace": "मोड मार्केटप्लेस", "settings": "मोड सेटिंग्स", - "description": "विशेष व्यक्तित्व जो Roo के व्यवहार को अनुकूलित करते हैं।" + "description": "विशेष व्यक्तित्व जो Roo के व्यवहार को अनुकूलित करते हैं।", + "searchPlaceholder": "मोड खोजें..." }, "errorReadingFile": "फ़ाइल पढ़ने में त्रुटि:", "noValidImages": "कोई मान्य चित्र प्रोसेस नहीं किया गया", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 2ef1cb75e7d..82efa55d598 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -122,7 +122,8 @@ "title": "Mode", "marketplace": "Marketplace Mode", "settings": "Pengaturan Mode", - "description": "Persona khusus yang menyesuaikan perilaku Roo." + "description": "Persona khusus yang menyesuaikan perilaku Roo.", + "searchPlaceholder": "Cari mode..." }, "addImages": "Tambahkan gambar ke pesan", "sendMessage": "Kirim pesan", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index eb3984f1bed..e6fbb196780 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -116,7 +116,8 @@ "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": "Cerca modalità..." }, "errorReadingFile": "Errore nella lettura del file:", "noValidImages": "Nessuna immagine valida è stata elaborata", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 2f6e6bfab73..e9a2bbac9cb 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -116,7 +116,8 @@ "title": "モード", "marketplace": "モードマーケットプレイス", "settings": "モード設定", - "description": "Rooの動作をカスタマイズする専門的なペルソナ。" + "description": "Rooの動作をカスタマイズする専門的なペルソナ。", + "searchPlaceholder": "モードを検索..." }, "errorReadingFile": "ファイル読み込みエラー:", "noValidImages": "有効な画像が処理されませんでした", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index f4a5c336026..2ba3758094d 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -116,7 +116,8 @@ "title": "모드", "marketplace": "모드 마켓플레이스", "settings": "모드 설정", - "description": "Roo의 행동을 맞춤화하는 전문화된 페르소나." + "description": "Roo의 행동을 맞춤화하는 전문화된 페르소나.", + "searchPlaceholder": "모드 검색..." }, "errorReadingFile": "파일 읽기 오류:", "noValidImages": "처리된 유효한 이미지가 없습니다", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 1d11db26680..fcbc02d1314 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -108,7 +108,8 @@ "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": "Zoek modi..." }, "addImages": "Afbeeldingen toevoegen aan bericht", "sendMessage": "Bericht verzenden", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 88c58418ad4..0f5616a4fe5 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -116,7 +116,8 @@ "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": "Szukaj trybów..." }, "errorReadingFile": "Błąd odczytu pliku:", "noValidImages": "Nie przetworzono żadnych prawidłowych obrazów", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 3784d6cc64d..1c770dbea9a 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -116,7 +116,8 @@ "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": "Pesquisar modos..." }, "errorReadingFile": "Erro ao ler arquivo:", "noValidImages": "Nenhuma imagem válida foi processada", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 0660d3e1d6d..5f41630dc3a 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -108,7 +108,8 @@ "title": "Режимы", "marketplace": "Маркетплейс режимов", "settings": "Настройки режимов", - "description": "Специализированные персоны, которые настраивают поведение Roo." + "description": "Специализированные персоны, которые настраивают поведение Roo.", + "searchPlaceholder": "Поиск режимов..." }, "addImages": "Добавить изображения к сообщению", "sendMessage": "Отправить сообщение", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 75bc126dffd..8e5f9afc341 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -116,7 +116,8 @@ "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": "Modları ara..." }, "errorReadingFile": "Dosya okuma hatası:", "noValidImages": "Hiçbir geçerli resim işlenmedi", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 944eabcb942..4b1c51bb4b1 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -116,7 +116,8 @@ "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": "Tìm kiếm chế độ..." }, "errorReadingFile": "Lỗi khi đọc tệp:", "noValidImages": "Không có hình ảnh hợp lệ nào được xử lý", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 616cd14fec1..0e9348b833f 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -116,7 +116,8 @@ "title": "模式", "marketplace": "模式市场", "settings": "模式设置", - "description": "专门定制Roo行为的角色。" + "description": "专门定制Roo行为的角色。", + "searchPlaceholder": "搜索模式..." }, "errorReadingFile": "读取文件时出错:", "noValidImages": "没有处理有效图片", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 662421900ed..0695e39961c 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -116,7 +116,8 @@ "title": "模式", "marketplace": "模式市集", "settings": "模式設定", - "description": "專門定制Roo行為的角色。" + "description": "專門定制Roo行為的角色。", + "searchPlaceholder": "搜尋模式..." }, "errorReadingFile": "讀取檔案時發生錯誤:", "noValidImages": "未處理到任何有效圖片", From c27f856f9cf2a172ca797df6cde049a9c34690b3 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Wed, 23 Jul 2025 17:52:55 -0600 Subject: [PATCH 4/8] fix: add translation for 'No results found' and auto-focus search input - Added translation key 'modeSelector.noResults' for the hardcoded 'No results found' text - Added auto-focus functionality to search input when mode selector popover opens - Updated all language files with appropriate translations --- webview-ui/src/components/chat/ModeSelector.tsx | 11 ++++++++++- webview-ui/src/i18n/locales/ca/chat.json | 3 ++- webview-ui/src/i18n/locales/de/chat.json | 3 ++- webview-ui/src/i18n/locales/en/chat.json | 3 ++- webview-ui/src/i18n/locales/es/chat.json | 3 ++- webview-ui/src/i18n/locales/fr/chat.json | 3 ++- webview-ui/src/i18n/locales/hi/chat.json | 3 ++- webview-ui/src/i18n/locales/id/chat.json | 3 ++- webview-ui/src/i18n/locales/it/chat.json | 3 ++- webview-ui/src/i18n/locales/ja/chat.json | 3 ++- webview-ui/src/i18n/locales/ko/chat.json | 3 ++- webview-ui/src/i18n/locales/nl/chat.json | 3 ++- webview-ui/src/i18n/locales/pl/chat.json | 3 ++- webview-ui/src/i18n/locales/pt-BR/chat.json | 3 ++- webview-ui/src/i18n/locales/ru/chat.json | 3 ++- webview-ui/src/i18n/locales/tr/chat.json | 3 ++- webview-ui/src/i18n/locales/vi/chat.json | 3 ++- webview-ui/src/i18n/locales/zh-CN/chat.json | 3 ++- webview-ui/src/i18n/locales/zh-TW/chat.json | 3 ++- 19 files changed, 46 insertions(+), 19 deletions(-) diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index 00a871f557e..ea25adece00 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -114,6 +114,13 @@ export const ModeSelector = ({ [trackModeSelectorOpened], ) + // Auto-focus search input when popover opens + React.useEffect(() => { + if (open && searchInputRef.current) { + searchInputRef.current.focus() + } + }, [open]) + // Combine instruction text for tooltip const instructionText = `${t("chat:modeSelector.description")} ${modeShortcutText}` @@ -172,7 +179,9 @@ export const ModeSelector = ({ {/* Mode List */}
{filteredModes.length === 0 && searchValue ? ( -
No results found
+
+ {t("chat:modeSelector.noResults")} +
) : (
{filteredModes.map((mode) => ( diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index e8c1d145cc0..9bd10e1972e 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -117,7 +117,8 @@ "marketplace": "Marketplace de Modes", "settings": "Configuració de Modes", "description": "Personalitats especialitzades que adapten el comportament de Roo.", - "searchPlaceholder": "Cerca modes..." + "searchPlaceholder": "Cerca modes...", + "noResults": "No s'han trobat resultats" }, "errorReadingFile": "Error en llegir el fitxer:", "noValidImages": "No s'ha processat cap imatge vàlida", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 277800d4ee3..27158a2da45 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -117,7 +117,8 @@ "marketplace": "Modus-Marketplace", "settings": "Modus-Einstellungen", "description": "Spezialisierte Personas, die Roos Verhalten anpassen.", - "searchPlaceholder": "Modi suchen..." + "searchPlaceholder": "Modi suchen...", + "noResults": "Keine Ergebnisse gefunden" }, "errorReadingFile": "Fehler beim Lesen der Datei:", "noValidImages": "Keine gültigen Bilder wurden verarbeitet", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 4fd374cfff8..7a8870db4ac 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -119,7 +119,8 @@ "marketplace": "Mode Marketplace", "settings": "Mode Settings", "description": "Specialized personas that tailor Roo's behavior.", - "searchPlaceholder": "Search modes..." + "searchPlaceholder": "Search modes...", + "noResults": "No results 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", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 5792333b9d9..17cac725158 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -117,7 +117,8 @@ "marketplace": "Marketplace de Modos", "settings": "Configuración de Modos", "description": "Personalidades especializadas que adaptan el comportamiento de Roo.", - "searchPlaceholder": "Buscar modos..." + "searchPlaceholder": "Buscar modos...", + "noResults": "No se encontraron resultados" }, "errorReadingFile": "Error al leer el archivo:", "noValidImages": "No se procesaron imágenes válidas", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index daa02759ea6..2e552c32c01 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -117,7 +117,8 @@ "marketplace": "Marketplace de Modes", "settings": "Paramètres des Modes", "description": "Personas spécialisés qui adaptent le comportement de Roo.", - "searchPlaceholder": "Rechercher des modes..." + "searchPlaceholder": "Rechercher des modes...", + "noResults": "Aucun résultat trouvé" }, "errorReadingFile": "Erreur lors de la lecture du fichier :", "noValidImages": "Aucune image valide n'a été traitée", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index eddc9097b79..530d33ac4cb 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -117,7 +117,8 @@ "marketplace": "मोड मार्केटप्लेस", "settings": "मोड सेटिंग्स", "description": "विशेष व्यक्तित्व जो Roo के व्यवहार को अनुकूलित करते हैं।", - "searchPlaceholder": "मोड खोजें..." + "searchPlaceholder": "मोड खोजें...", + "noResults": "कोई परिणाम नहीं मिला" }, "errorReadingFile": "फ़ाइल पढ़ने में त्रुटि:", "noValidImages": "कोई मान्य चित्र प्रोसेस नहीं किया गया", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 82efa55d598..1e89d4f1178 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -123,7 +123,8 @@ "marketplace": "Marketplace Mode", "settings": "Pengaturan Mode", "description": "Persona khusus yang menyesuaikan perilaku Roo.", - "searchPlaceholder": "Cari mode..." + "searchPlaceholder": "Cari mode...", + "noResults": "Tidak ada hasil yang ditemukan" }, "addImages": "Tambahkan gambar ke pesan", "sendMessage": "Kirim pesan", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index e6fbb196780..a1efb4c9e52 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -117,7 +117,8 @@ "marketplace": "Marketplace delle Modalità", "settings": "Impostazioni Modalità", "description": "Personalità specializzate che adattano il comportamento di Roo.", - "searchPlaceholder": "Cerca modalità..." + "searchPlaceholder": "Cerca modalità...", + "noResults": "Nessun risultato trovato" }, "errorReadingFile": "Errore nella lettura del file:", "noValidImages": "Nessuna immagine valida è stata elaborata", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index e9a2bbac9cb..88130ce6ac8 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -117,7 +117,8 @@ "marketplace": "モードマーケットプレイス", "settings": "モード設定", "description": "Rooの動作をカスタマイズする専門的なペルソナ。", - "searchPlaceholder": "モードを検索..." + "searchPlaceholder": "モードを検索...", + "noResults": "結果が見つかりません" }, "errorReadingFile": "ファイル読み込みエラー:", "noValidImages": "有効な画像が処理されませんでした", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 2ba3758094d..778600d1ec9 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -117,7 +117,8 @@ "marketplace": "모드 마켓플레이스", "settings": "모드 설정", "description": "Roo의 행동을 맞춤화하는 전문화된 페르소나.", - "searchPlaceholder": "모드 검색..." + "searchPlaceholder": "모드 검색...", + "noResults": "결과를 찾을 수 없습니다" }, "errorReadingFile": "파일 읽기 오류:", "noValidImages": "처리된 유효한 이미지가 없습니다", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index fcbc02d1314..f6ed85a7b48 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -109,7 +109,8 @@ "marketplace": "Modus Marktplaats", "settings": "Modus Instellingen", "description": "Gespecialiseerde persona's die het gedrag van Roo aanpassen.", - "searchPlaceholder": "Zoek modi..." + "searchPlaceholder": "Zoek modi...", + "noResults": "Geen resultaten gevonden" }, "addImages": "Afbeeldingen toevoegen aan bericht", "sendMessage": "Bericht verzenden", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 0f5616a4fe5..00eb511d80e 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -117,7 +117,8 @@ "marketplace": "Marketplace Trybów", "settings": "Ustawienia Trybów", "description": "Wyspecjalizowane persony, które dostosowują zachowanie Roo.", - "searchPlaceholder": "Szukaj trybów..." + "searchPlaceholder": "Szukaj trybów...", + "noResults": "Nie znaleziono wyników" }, "errorReadingFile": "Błąd odczytu pliku:", "noValidImages": "Nie przetworzono żadnych prawidłowych obrazów", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 1c770dbea9a..f9eb87ae036 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -117,7 +117,8 @@ "marketplace": "Marketplace de Modos", "settings": "Configurações de Modos", "description": "Personas especializadas que adaptam o comportamento do Roo.", - "searchPlaceholder": "Pesquisar modos..." + "searchPlaceholder": "Pesquisar modos...", + "noResults": "Nenhum resultado encontrado" }, "errorReadingFile": "Erro ao ler arquivo:", "noValidImages": "Nenhuma imagem válida foi processada", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 5f41630dc3a..9e3ee5f5364 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -109,7 +109,8 @@ "marketplace": "Маркетплейс режимов", "settings": "Настройки режимов", "description": "Специализированные персоны, которые настраивают поведение Roo.", - "searchPlaceholder": "Поиск режимов..." + "searchPlaceholder": "Поиск режимов...", + "noResults": "Ничего не найдено" }, "addImages": "Добавить изображения к сообщению", "sendMessage": "Отправить сообщение", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 8e5f9afc341..bd0c984295c 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -117,7 +117,8 @@ "marketplace": "Mod Pazaryeri", "settings": "Mod Ayarları", "description": "Roo'nun davranışını özelleştiren uzmanlaşmış kişilikler.", - "searchPlaceholder": "Modları ara..." + "searchPlaceholder": "Modları ara...", + "noResults": "Sonuç bulunamadı" }, "errorReadingFile": "Dosya okuma hatası:", "noValidImages": "Hiçbir geçerli resim işlenmedi", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 4b1c51bb4b1..83f033b0454 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -117,7 +117,8 @@ "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.", - "searchPlaceholder": "Tìm kiếm chế độ..." + "searchPlaceholder": "Tìm kiếm chế độ...", + "noResults": "Không tìm thấy kết quả nào" }, "errorReadingFile": "Lỗi khi đọc tệp:", "noValidImages": "Không có hình ảnh hợp lệ nào được xử lý", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 0e9348b833f..bbc7f034634 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -117,7 +117,8 @@ "marketplace": "模式市场", "settings": "模式设置", "description": "专门定制Roo行为的角色。", - "searchPlaceholder": "搜索模式..." + "searchPlaceholder": "搜索模式...", + "noResults": "未找到结果" }, "errorReadingFile": "读取文件时出错:", "noValidImages": "没有处理有效图片", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 0695e39961c..b20d1642a0e 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -117,7 +117,8 @@ "marketplace": "模式市集", "settings": "模式設定", "description": "專門定制Roo行為的角色。", - "searchPlaceholder": "搜尋模式..." + "searchPlaceholder": "搜尋模式...", + "noResults": "沒有找到結果" }, "errorReadingFile": "讀取檔案時發生錯誤:", "noValidImages": "未處理到任何有效圖片", From be8a56047123bf4c41da3676f05e984349ec255a Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Wed, 23 Jul 2025 20:50:53 -0600 Subject: [PATCH 5/8] fix: adjust right spacing in mode selector bottom bar to match left side --- webview-ui/src/components/chat/ModeSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index ea25adece00..4f53c28a6a9 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -243,7 +243,7 @@ export const ModeSelector = ({
{/* Info icon and title on the right with matching spacing */} -
+
From 2b0017e89d6acfaf6dc6c2410850bd888509be6d Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Thu, 24 Jul 2025 12:31:11 -0500 Subject: [PATCH 6/8] fix: remove requestAnimationFrame and improve search priority - Replace requestAnimationFrame with direct state updates for better clarity - Implement two-tier search: prioritize mode names/slugs over descriptions - Mode names now always appear before description matches in search results --- .../src/components/chat/ModeSelector.tsx | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index 4f53c28a6a9..2defe6d2750 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -64,28 +64,55 @@ export const ModeSelector = ({ // Find the selected mode const selectedMode = React.useMemo(() => modes.find((mode) => mode.slug === value), [modes, value]) - // Memoize searchable items for fuzzy search - const searchableItems = React.useMemo(() => { + // Memoize searchable items for fuzzy search with separate name and description search + const nameSearchItems = React.useMemo(() => { return modes.map((mode) => ({ original: mode, - searchStr: [mode.name, mode.slug, mode.description].filter(Boolean).join(" "), + searchStr: [mode.name, mode.slug].filter(Boolean).join(" "), })) }, [modes]) - // Create a memoized Fzf instance - const fzfInstance = React.useMemo(() => { - return new Fzf(searchableItems, { + const descriptionSearchItems = React.useMemo(() => { + return modes.map((mode) => ({ + original: mode, + searchStr: mode.description || "", + })) + }, [modes]) + + // Create memoized Fzf instances for name and description searches + const nameFzfInstance = React.useMemo(() => { + return new Fzf(nameSearchItems, { selector: (item) => item.searchStr, }) - }, [searchableItems]) + }, [nameSearchItems]) - // Filter modes based on search value using fuzzy search + const descriptionFzfInstance = React.useMemo(() => { + return new Fzf(descriptionSearchItems, { + selector: (item) => item.searchStr, + }) + }, [descriptionSearchItems]) + + // Filter modes based on search value using fuzzy search with priority const filteredModes = React.useMemo(() => { if (!searchValue) return modes - const matchingItems = fzfInstance.find(searchValue).map((result) => result.item.original) - return matchingItems - }, [modes, searchValue, fzfInstance]) + // First search in names/slugs + const nameMatches = nameFzfInstance.find(searchValue) + const nameMatchedModes = new Set(nameMatches.map((result) => result.item.original.slug)) + + // Then search in descriptions + const descriptionMatches = descriptionFzfInstance.find(searchValue) + + // Combine results: name matches first, then description matches + const combinedResults = [ + ...nameMatches.map((result) => result.item.original), + ...descriptionMatches + .filter((result) => !nameMatchedModes.has(result.item.original.slug)) + .map((result) => result.item.original), + ] + + return combinedResults + }, [modes, searchValue, nameFzfInstance, descriptionFzfInstance]) const onClearSearch = React.useCallback(() => { setSearchValue("") @@ -97,7 +124,7 @@ export const ModeSelector = ({ onChange(modeSlug as Mode) setOpen(false) // Clear search after selection - requestAnimationFrame(() => setSearchValue("")) + setSearchValue("") }, [onChange], ) @@ -108,7 +135,7 @@ export const ModeSelector = ({ setOpen(isOpen) // Clear search when closing if (!isOpen) { - requestAnimationFrame(() => setSearchValue("")) + setSearchValue("") } }, [trackModeSelectorOpened], From 4ea6164d5033a17627ccfcc687dc4c9ae37e4fb2 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Sat, 26 Jul 2025 15:43:17 -0600 Subject: [PATCH 7/8] feat: conditionally show search bar in ModeSelector based on item count - Search bar now only appears when there are more than 6 modes - When 6 or fewer modes, display info blurb instead of search bar - Hide info icon when search bar is not visible - Add comprehensive tests for the new behavior --- .../src/components/chat/ModeSelector.tsx | 56 ++++++----- .../chat/__tests__/ModeSelector.spec.tsx | 94 ++++++++++++++++++- 2 files changed, 125 insertions(+), 25 deletions(-) diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index 2defe6d2750..b45e28c559d 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -182,26 +182,32 @@ export const ModeSelector = ({ container={portalContainer} className="p-0 overflow-hidden min-w-80 max-w-9/10">
- {/* Search input only */} -
- setSearchValue(e.target.value)} - placeholder={t("chat:modeSelector.searchPlaceholder")} - className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0" - data-testid="mode-search-input" - /> - {searchValue.length > 0 && ( -
- -
- )} -
+ {/* Show search bar only when there are more than 6 items, otherwise show info blurb */} + {modes.length > 6 ? ( +
+ setSearchValue(e.target.value)} + placeholder={t("chat:modeSelector.searchPlaceholder")} + className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0" + data-testid="mode-search-input" + /> + {searchValue.length > 0 && ( +
+ +
+ )} +
+ ) : ( +
+

{instructionText}

+
+ )} {/* Mode List */}
@@ -269,11 +275,13 @@ export const ModeSelector = ({ />
- {/* Info icon and title on the right with matching spacing */} + {/* Info icon and title on the right - only show info icon when search bar is visible */}
- - - + {modes.length > 6 && ( + + + + )}

{t("chat:modeSelector.title")}

diff --git a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx index d6fc81368d1..4f2a5277ca0 100644 --- a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx @@ -1,8 +1,9 @@ import React from "react" -import { render, screen } from "@/utils/test-utils" +import { render, screen, fireEvent } from "@/utils/test-utils" import { describe, test, expect, vi } from "vitest" import ModeSelector from "../ModeSelector" import { Mode } from "@roo/modes" +import { ModeConfig } from "@roo-code/types" // Mock the dependencies vi.mock("@/utils/vscode", () => ({ @@ -28,6 +29,23 @@ vi.mock("@/components/ui/hooks/useRooPortal", () => ({ useRooPortal: () => document.body, })) +vi.mock("@/utils/TelemetryClient", () => ({ + telemetryClient: { + capture: vi.fn(), + }, +})) + +// Create a variable to control what getAllModes returns +let mockModes: ModeConfig[] = [] + +vi.mock("@roo/modes", async () => { + const actual = await vi.importActual("@roo/modes") + return { + ...actual, + getAllModes: () => mockModes, + } +}) + describe("ModeSelector", () => { test("shows custom description from customModePrompts", () => { const customModePrompts = { @@ -55,4 +73,78 @@ describe("ModeSelector", () => { // The component should be rendered expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument() }) + + test("shows search bar when there are more than 6 modes", () => { + // Set up mock to return 7 modes + mockModes = Array.from({ length: 7 }, (_, i) => ({ + slug: `mode-${i}`, + name: `Mode ${i}`, + description: `Description for mode ${i}`, + roleDefinition: "Role definition", + groups: ["read", "edit"], + })) + + render() + + // Click to open the popover + fireEvent.click(screen.getByTestId("mode-selector-trigger")) + + // Search input should be visible + expect(screen.getByTestId("mode-search-input")).toBeInTheDocument() + + // Info icon should be visible + expect(screen.getByText("chat:modeSelector.title")).toBeInTheDocument() + const infoIcon = document.querySelector(".codicon-info") + expect(infoIcon).toBeInTheDocument() + }) + + test("shows info blurb instead of search bar when there are 6 or fewer modes", () => { + // Set up mock to return 5 modes + mockModes = Array.from({ length: 5 }, (_, i) => ({ + slug: `mode-${i}`, + name: `Mode ${i}`, + description: `Description for mode ${i}`, + roleDefinition: "Role definition", + groups: ["read", "edit"], + })) + + render() + + // Click to open the popover + fireEvent.click(screen.getByTestId("mode-selector-trigger")) + + // Search input should NOT be visible + expect(screen.queryByTestId("mode-search-input")).not.toBeInTheDocument() + + // Info blurb should be visible + expect(screen.getByText(/chat:modeSelector.description/)).toBeInTheDocument() + + // Info icon should NOT be visible + const infoIcon = document.querySelector(".codicon-info") + expect(infoIcon).not.toBeInTheDocument() + }) + + test("filters modes correctly when searching", () => { + // Set up mock to return 7 modes to enable search + mockModes = Array.from({ length: 7 }, (_, i) => ({ + slug: `mode-${i}`, + name: `Mode ${i}`, + description: `Description for mode ${i}`, + roleDefinition: "Role definition", + groups: ["read", "edit"], + })) + + render() + + // Click to open the popover + fireEvent.click(screen.getByTestId("mode-selector-trigger")) + + // Type in search + const searchInput = screen.getByTestId("mode-search-input") + fireEvent.change(searchInput, { target: { value: "Mode 3" } }) + + // Should show filtered results + const modeItems = screen.getAllByTestId("mode-selector-item") + expect(modeItems.length).toBeLessThan(7) // Should have filtered some out + }) }) From 73570e0a142b9116b2c49399999801f5d5d5e419 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Sat, 26 Jul 2025 16:00:34 -0600 Subject: [PATCH 8/8] refactor: improve ModeSelector based on PR review feedback - Extract magic number 6 to SEARCH_THRESHOLD constant for clarity - Add optional disableSearch prop for consistency with SelectDropdown - Use showSearch variable to control search visibility logic - Add comprehensive tests for disableSearch prop behavior - Fix redundant tooltip when search is hidden These changes improve maintainability and provide more flexible control over the search functionality while maintaining backward compatibility. --- .../src/components/chat/ModeSelector.tsx | 14 +++-- .../chat/__tests__/ModeSelector.spec.tsx | 52 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index b45e28c559d..93dd2f1f4fd 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -13,6 +13,9 @@ import { telemetryClient } from "@/utils/TelemetryClient" import { TelemetryEventName } from "@roo-code/types" import { Fzf } from "fzf" +// Minimum number of modes required to show search functionality +const SEARCH_THRESHOLD = 6 + interface ModeSelectorProps { value: Mode onChange: (value: Mode) => void @@ -22,6 +25,7 @@ interface ModeSelectorProps { modeShortcutText: string customModes?: ModeConfig[] customModePrompts?: CustomModePrompts + disableSearch?: boolean } export const ModeSelector = ({ @@ -33,6 +37,7 @@ export const ModeSelector = ({ modeShortcutText, customModes, customModePrompts, + disableSearch = false, }: ModeSelectorProps) => { const [open, setOpen] = React.useState(false) const [searchValue, setSearchValue] = React.useState("") @@ -148,6 +153,9 @@ export const ModeSelector = ({ } }, [open]) + // Determine if search should be shown + const showSearch = !disableSearch && modes.length > SEARCH_THRESHOLD + // Combine instruction text for tooltip const instructionText = `${t("chat:modeSelector.description")} ${modeShortcutText}` @@ -182,8 +190,8 @@ export const ModeSelector = ({ container={portalContainer} className="p-0 overflow-hidden min-w-80 max-w-9/10">
- {/* Show search bar only when there are more than 6 items, otherwise show info blurb */} - {modes.length > 6 ? ( + {/* Show search bar only when there are more than SEARCH_THRESHOLD items, otherwise show info blurb */} + {showSearch ? (
- {modes.length > 6 && ( + {showSearch && ( diff --git a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx index 4f2a5277ca0..a8291688936 100644 --- a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx @@ -147,4 +147,56 @@ describe("ModeSelector", () => { const modeItems = screen.getAllByTestId("mode-selector-item") expect(modeItems.length).toBeLessThan(7) // Should have filtered some out }) + + test("respects disableSearch prop even when there are more than 6 modes", () => { + // Set up mock to return 10 modes + mockModes = Array.from({ length: 10 }, (_, i) => ({ + slug: `mode-${i}`, + name: `Mode ${i}`, + description: `Description for mode ${i}`, + roleDefinition: "Role definition", + groups: ["read", "edit"], + })) + + render( + , + ) + + // Click to open the popover + fireEvent.click(screen.getByTestId("mode-selector-trigger")) + + // Search input should NOT be visible even with 10 modes + expect(screen.queryByTestId("mode-search-input")).not.toBeInTheDocument() + + // Info blurb should be visible instead + expect(screen.getByText(/chat:modeSelector.description/)).toBeInTheDocument() + + // Info icon should NOT be visible + const infoIcon = document.querySelector(".codicon-info") + expect(infoIcon).not.toBeInTheDocument() + }) + + test("shows search when disableSearch is false (default) and modes > 6", () => { + // Set up mock to return 8 modes + mockModes = Array.from({ length: 8 }, (_, i) => ({ + slug: `mode-${i}`, + name: `Mode ${i}`, + description: `Description for mode ${i}`, + roleDefinition: "Role definition", + groups: ["read", "edit"], + })) + + // Don't pass disableSearch prop (should default to false) + render() + + // Click to open the popover + fireEvent.click(screen.getByTestId("mode-selector-trigger")) + + // Search input should be visible + expect(screen.getByTestId("mode-search-input")).toBeInTheDocument() + + // Info icon should be visible + const infoIcon = document.querySelector(".codicon-info") + expect(infoIcon).toBeInTheDocument() + }) })