From 8318d3e8a953483816d4d94fe3c75e4c11f0c661 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Wed, 23 Jul 2025 20:57:46 -0600 Subject: [PATCH 01/11] feat: sync API config selector style with mode selector from PR #6140 - Add search functionality at the top with fuzzy search - Move settings button to bottom left - Add title and info icon on bottom right - Match exact layout and spacing from ModeSelector - Create new ApiConfigSelector component - Update ChatTextArea to use new component - Remove unused imports to fix linting --- .../src/components/chat/ApiConfigSelector.tsx | 225 ++++++++++++++++++ .../src/components/chat/ChatTextArea.tsx | 129 +--------- 2 files changed, 234 insertions(+), 120 deletions(-) create mode 100644 webview-ui/src/components/chat/ApiConfigSelector.tsx diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx new file mode 100644 index 00000000000..c2f8e5f1dd1 --- /dev/null +++ b/webview-ui/src/components/chat/ApiConfigSelector.tsx @@ -0,0 +1,225 @@ +import React, { useState, useMemo, useCallback } from "react" +import { cn } from "@/lib/utils" +import { useRooPortal } from "@/components/ui/hooks/useRooPortal" +import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" +import { IconButton } from "./IconButton" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { vscode } from "@/utils/vscode" +import { Fzf } from "fzf" +import { Check, X, Pin } from "lucide-react" +import { Button } from "@/components/ui" + +interface ApiConfigSelectorProps { + value: string + displayName: string + disabled?: boolean + title?: string + onChange: (value: string) => void + triggerClassName?: string + listApiConfigMeta: Array<{ id: string; name: string }> + pinnedApiConfigs?: Record + togglePinnedApiConfig: (id: string) => void +} + +export const ApiConfigSelector = ({ + value, + displayName, + disabled = false, + title = "", + onChange, + triggerClassName = "", + listApiConfigMeta, + pinnedApiConfigs, + togglePinnedApiConfig, +}: ApiConfigSelectorProps) => { + const { t } = useAppTranslation() + const [open, setOpen] = useState(false) + const [searchValue, setSearchValue] = useState("") + const portalContainer = useRooPortal("roo-portal") + + // Create searchable items for fuzzy search + const searchableItems = useMemo(() => { + return listApiConfigMeta.map((config) => ({ + original: config, + searchStr: config.name, + })) + }, [listApiConfigMeta]) + + // Create Fzf instance + const fzfInstance = useMemo(() => { + return new Fzf(searchableItems, { + selector: (item) => item.searchStr, + }) + }, [searchableItems]) + + // Filter configs based on search + const filteredConfigs = useMemo(() => { + if (!searchValue) return listApiConfigMeta + + const matchingItems = fzfInstance.find(searchValue).map((result) => result.item.original) + return matchingItems + }, [listApiConfigMeta, searchValue, fzfInstance]) + + // Separate pinned and unpinned configs + const { pinnedConfigs, unpinnedConfigs } = useMemo(() => { + const pinned = filteredConfigs.filter((config) => pinnedApiConfigs?.[config.id]) + const unpinned = filteredConfigs.filter((config) => !pinnedApiConfigs?.[config.id]) + return { pinnedConfigs: pinned, unpinnedConfigs: unpinned } + }, [filteredConfigs, pinnedApiConfigs]) + + const handleSelect = useCallback( + (configId: string) => { + onChange(configId) + setOpen(false) + setSearchValue("") + }, + [onChange], + ) + + const handleEditClick = useCallback(() => { + vscode.postMessage({ + type: "switchTab", + tab: "settings", + }) + setOpen(false) + }, []) + + const renderConfigItem = useCallback( + (config: { id: string; name: string }, isPinned: boolean) => { + const isCurrentConfig = config.id === value + + return ( +
handleSelect(config.id)} + className={cn( + "px-3 py-1.5 text-sm cursor-pointer flex items-center group", + "hover:bg-vscode-list-hoverBackground", + isCurrentConfig && + "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground", + )}> + {config.name} +
+ {isCurrentConfig && ( +
+ +
+ )} + + + +
+
+ ) + }, + [value, handleSelect, t, togglePinnedApiConfig], + ) + + const triggerContent = ( + + {displayName} + + ) + + return ( + + {title ? {triggerContent} : triggerContent} + +
+ {/* Search input */} +
+ setSearchValue(e.target.value)} + 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" + autoFocus + /> + {searchValue.length > 0 && ( +
+ setSearchValue("")} + /> +
+ )} +
+ + {/* Config list */} +
+ {filteredConfigs.length === 0 && searchValue ? ( +
+ {t("common:ui.no_results")} +
+ ) : ( +
+ {/* Pinned configs */} + {pinnedConfigs.map((config) => renderConfigItem(config, true))} + + {/* Separator between pinned and unpinned */} + {pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 && ( +
+ )} + + {/* Unpinned configs */} + {unpinnedConfigs.map((config) => renderConfigItem(config, false))} +
+ )} +
+ + {/* Bottom bar with buttons on left and title on right */} +
+
+ +
+ + {/* Info icon and title on the right with matching spacing */} +
+ + + +

+ {t("prompts:apiConfiguration.title")} +

+
+
+
+ + + ) +} diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 6c541353eb2..6fff345be72 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -19,13 +19,14 @@ import { SearchResult, } from "@src/utils/context-mentions" import { convertToMentionPath } from "@/utils/path-mentions" -import { SelectDropdown, DropdownOptionType, Button, StandardTooltip } from "@/components/ui" +import { StandardTooltip } from "@/components/ui" import Thumbnails from "../common/Thumbnails" import ModeSelector from "./ModeSelector" +import { ApiConfigSelector } from "./ApiConfigSelector" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" -import { VolumeX, Pin, Check, Image, WandSparkles, SendHorizontal } from "lucide-react" +import { VolumeX, Image, WandSparkles, SendHorizontal } from "lucide-react" import { IndexingStatusBadge } from "./IndexingStatusBadge" import { cn } from "@/lib/utils" import { usePromptHistory } from "./hooks/usePromptHistory" @@ -824,122 +825,11 @@ const ChatTextArea = forwardRef( /> ) - // Helper function to get API config dropdown options - const getApiConfigOptions = useMemo(() => { - const pinnedConfigs = (listApiConfigMeta || []) - .filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id]) - .map((config) => ({ - value: config.id, - label: config.name, - name: config.name, - type: DropdownOptionType.ITEM, - pinned: true, - })) - .sort((a, b) => a.label.localeCompare(b.label)) - - const unpinnedConfigs = (listApiConfigMeta || []) - .filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id]) - .map((config) => ({ - value: config.id, - label: config.name, - name: config.name, - type: DropdownOptionType.ITEM, - pinned: false, - })) - .sort((a, b) => a.label.localeCompare(b.label)) - - const hasPinnedAndUnpinned = pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 - - return [ - ...pinnedConfigs, - ...(hasPinnedAndUnpinned - ? [ - { - value: "sep-pinned", - label: t("chat:separator"), - type: DropdownOptionType.SEPARATOR, - }, - ] - : []), - ...unpinnedConfigs, - { - value: "sep-2", - label: t("chat:separator"), - type: DropdownOptionType.SEPARATOR, - }, - { - value: "settingsButtonClicked", - label: t("chat:edit"), - type: DropdownOptionType.ACTION, - }, - ] - }, [listApiConfigMeta, pinnedApiConfigs, t]) - // Helper function to handle API config change const handleApiConfigChange = useCallback((value: string) => { - if (value === "settingsButtonClicked") { - vscode.postMessage({ - type: "loadApiConfiguration", - text: value, - values: { section: "providers" }, - }) - } else { - vscode.postMessage({ type: "loadApiConfigurationById", text: value }) - } + vscode.postMessage({ type: "loadApiConfigurationById", text: value }) }, []) - // Helper function to render API config item - const renderApiConfigItem = useCallback( - ({ type, value, label, pinned }: any) => { - if (type !== DropdownOptionType.ITEM) { - return label - } - - const config = listApiConfigMeta?.find((c) => c.id === value) - const isCurrentConfig = config?.name === currentApiConfigName - - return ( -
-
- {label} -
-
-
- -
- - - -
-
- ) - }, - [listApiConfigMeta, currentApiConfigName, t, togglePinnedApiConfig], - ) - // Helper function to render non-edit mode controls const renderNonEditModeControls = () => (
@@ -947,17 +837,16 @@ const ChatTextArea = forwardRef(
{renderModeSelector()}
-
From af1d180bf1e759c47aacb08752f96c39779d3ddc Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Wed, 23 Jul 2025 21:03:30 -0600 Subject: [PATCH 02/11] fix: add missing no_results translation to common namespace --- webview-ui/src/i18n/locales/en/common.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/i18n/locales/en/common.json b/webview-ui/src/i18n/locales/en/common.json index ab6b3bc07e7..af0dfcdf803 100644 --- a/webview-ui/src/i18n/locales/en/common.json +++ b/webview-ui/src/i18n/locales/en/common.json @@ -20,7 +20,8 @@ "billion_suffix": "b" }, "ui": { - "search_placeholder": "Search..." + "search_placeholder": "Search...", + "no_results": "No results found" }, "mermaid": { "loading": "Generating mermaid diagram...", From e67eb5d6fbc24ba58765966d24164ea8c8c54e2e Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Wed, 23 Jul 2025 21:10:17 -0600 Subject: [PATCH 03/11] fix: restore search functionality to match original SelectDropdown behavior - Search now includes both config name and ID fields - Matches the original behavior where both label and value were searchable --- webview-ui/src/components/chat/ApiConfigSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx index c2f8e5f1dd1..e93579a1a28 100644 --- a/webview-ui/src/components/chat/ApiConfigSelector.tsx +++ b/webview-ui/src/components/chat/ApiConfigSelector.tsx @@ -41,7 +41,7 @@ export const ApiConfigSelector = ({ const searchableItems = useMemo(() => { return listApiConfigMeta.map((config) => ({ original: config, - searchStr: config.name, + searchStr: [config.name, config.id].filter(Boolean).join(" "), })) }, [listApiConfigMeta]) From a1d93ff686864aa7339082e746f9c946cc7f0176 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Wed, 23 Jul 2025 21:24:38 -0600 Subject: [PATCH 04/11] revert: restore original SelectDropdown for API config selector - Remove custom ApiConfigSelector component - Restore original SelectDropdown with built-in search functionality - Add back getApiConfigOptions and renderApiConfigItem functions - Maintain all original search behavior from SelectDropdown component --- .../src/components/chat/ChatTextArea.tsx | 127 ++++++++++++++++-- webview-ui/src/i18n/locales/ca/common.json | 3 +- webview-ui/src/i18n/locales/de/common.json | 3 +- webview-ui/src/i18n/locales/es/common.json | 3 +- webview-ui/src/i18n/locales/fr/common.json | 3 +- webview-ui/src/i18n/locales/hi/common.json | 3 +- webview-ui/src/i18n/locales/id/common.json | 3 +- webview-ui/src/i18n/locales/it/common.json | 3 +- webview-ui/src/i18n/locales/ja/common.json | 3 +- webview-ui/src/i18n/locales/ko/common.json | 3 +- webview-ui/src/i18n/locales/nl/common.json | 3 +- webview-ui/src/i18n/locales/pl/common.json | 3 +- webview-ui/src/i18n/locales/pt-BR/common.json | 3 +- webview-ui/src/i18n/locales/ru/common.json | 3 +- webview-ui/src/i18n/locales/tr/common.json | 3 +- webview-ui/src/i18n/locales/vi/common.json | 3 +- webview-ui/src/i18n/locales/zh-CN/common.json | 3 +- webview-ui/src/i18n/locales/zh-TW/common.json | 3 +- 18 files changed, 152 insertions(+), 26 deletions(-) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 6fff345be72..6a6e24c3e2c 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -19,14 +19,13 @@ import { SearchResult, } from "@src/utils/context-mentions" import { convertToMentionPath } from "@/utils/path-mentions" -import { StandardTooltip } from "@/components/ui" +import { SelectDropdown, DropdownOptionType, Button, StandardTooltip } from "@/components/ui" import Thumbnails from "../common/Thumbnails" import ModeSelector from "./ModeSelector" -import { ApiConfigSelector } from "./ApiConfigSelector" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" -import { VolumeX, Image, WandSparkles, SendHorizontal } from "lucide-react" +import { VolumeX, Image, WandSparkles, SendHorizontal, Check, Pin } from "lucide-react" import { IndexingStatusBadge } from "./IndexingStatusBadge" import { cn } from "@/lib/utils" import { usePromptHistory } from "./hooks/usePromptHistory" @@ -827,9 +826,118 @@ const ChatTextArea = forwardRef( // Helper function to handle API config change const handleApiConfigChange = useCallback((value: string) => { - vscode.postMessage({ type: "loadApiConfigurationById", text: value }) + if (value === "settingsButtonClicked") { + vscode.postMessage({ + type: "loadApiConfiguration", + text: value, + values: { section: "providers" }, + }) + } else { + vscode.postMessage({ type: "loadApiConfigurationById", text: value }) + } }, []) + // Helper function to get API config options + const getApiConfigOptions = useMemo(() => { + const pinnedConfigs = (listApiConfigMeta || []) + .filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id]) + .map((config) => ({ + value: config.id, + label: config.name, + name: config.name, + type: DropdownOptionType.ITEM, + pinned: true, + })) + .sort((a, b) => a.label.localeCompare(b.label)) + + const unpinnedConfigs = (listApiConfigMeta || []) + .filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id]) + .map((config) => ({ + value: config.id, + label: config.name, + name: config.name, + type: DropdownOptionType.ITEM, + pinned: false, + })) + .sort((a, b) => a.label.localeCompare(b.label)) + + return [ + ...pinnedConfigs, + ...(pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 + ? [ + { + value: "sep-1", + label: t("chat:separator"), + type: DropdownOptionType.SEPARATOR, + }, + ] + : []), + ...unpinnedConfigs, + { + value: "sep-2", + label: t("chat:separator"), + type: DropdownOptionType.SEPARATOR, + }, + { + value: "settingsButtonClicked", + label: t("chat:edit"), + type: DropdownOptionType.ACTION, + }, + ] + }, [listApiConfigMeta, pinnedApiConfigs, t]) + + // Helper function to render API config item + const renderApiConfigItem = useCallback( + ({ type, value, label, pinned }: any) => { + if (type !== DropdownOptionType.ITEM) { + return label + } + + const config = listApiConfigMeta?.find((c) => c.id === value) + const isCurrentConfig = config?.name === currentApiConfigName + + return ( +
+
+ {label} +
+
+
+ +
+ + + +
+
+ ) + }, + [listApiConfigMeta, currentApiConfigName, t, togglePinnedApiConfig], + ) + // Helper function to render non-edit mode controls const renderNonEditModeControls = () => (
@@ -837,16 +945,17 @@ const ChatTextArea = forwardRef(
{renderModeSelector()}
-
diff --git a/webview-ui/src/i18n/locales/ca/common.json b/webview-ui/src/i18n/locales/ca/common.json index a7adfc6d209..13ad7a2ca70 100644 --- a/webview-ui/src/i18n/locales/ca/common.json +++ b/webview-ui/src/i18n/locales/ca/common.json @@ -20,7 +20,8 @@ "billion_suffix": "b" }, "ui": { - "search_placeholder": "Cerca..." + "search_placeholder": "Cerca...", + "no_results": "No s'han trobat resultats" }, "mermaid": { "loading": "Generant diagrama mermaid...", diff --git a/webview-ui/src/i18n/locales/de/common.json b/webview-ui/src/i18n/locales/de/common.json index f2a4491534d..c873e25d129 100644 --- a/webview-ui/src/i18n/locales/de/common.json +++ b/webview-ui/src/i18n/locales/de/common.json @@ -20,7 +20,8 @@ "billion_suffix": "b" }, "ui": { - "search_placeholder": "Suchen..." + "search_placeholder": "Suchen...", + "no_results": "Keine Ergebnisse gefunden" }, "mermaid": { "loading": "Mermaid-Diagramm wird generiert...", diff --git a/webview-ui/src/i18n/locales/es/common.json b/webview-ui/src/i18n/locales/es/common.json index b4dce90c986..ee0a924d43f 100644 --- a/webview-ui/src/i18n/locales/es/common.json +++ b/webview-ui/src/i18n/locales/es/common.json @@ -20,7 +20,8 @@ "billion_suffix": "b" }, "ui": { - "search_placeholder": "Buscar..." + "search_placeholder": "Buscar...", + "no_results": "No se encontraron resultados" }, "mermaid": { "loading": "Generando diagrama mermaid...", diff --git a/webview-ui/src/i18n/locales/fr/common.json b/webview-ui/src/i18n/locales/fr/common.json index c451040cd89..40c12e2afba 100644 --- a/webview-ui/src/i18n/locales/fr/common.json +++ b/webview-ui/src/i18n/locales/fr/common.json @@ -20,7 +20,8 @@ "billion_suffix": "b" }, "ui": { - "search_placeholder": "Rechercher..." + "search_placeholder": "Rechercher...", + "no_results": "Aucun résultat trouvé" }, "mermaid": { "loading": "Génération du diagramme mermaid...", diff --git a/webview-ui/src/i18n/locales/hi/common.json b/webview-ui/src/i18n/locales/hi/common.json index bef23aaecc3..227e25637e8 100644 --- a/webview-ui/src/i18n/locales/hi/common.json +++ b/webview-ui/src/i18n/locales/hi/common.json @@ -20,7 +20,8 @@ "billion_suffix": "b" }, "ui": { - "search_placeholder": "खोजें..." + "search_placeholder": "खोजें...", + "no_results": "कोई परिणाम नहीं मिला" }, "mermaid": { "loading": "मरमेड डायग्राम जनरेट हो रहा है...", diff --git a/webview-ui/src/i18n/locales/id/common.json b/webview-ui/src/i18n/locales/id/common.json index 42ab59a7999..3a3a3f5a78d 100644 --- a/webview-ui/src/i18n/locales/id/common.json +++ b/webview-ui/src/i18n/locales/id/common.json @@ -20,7 +20,8 @@ "billion_suffix": "m" }, "ui": { - "search_placeholder": "Cari..." + "search_placeholder": "Cari...", + "no_results": "Tidak ada hasil yang ditemukan" }, "mermaid": { "loading": "Membuat diagram mermaid...", diff --git a/webview-ui/src/i18n/locales/it/common.json b/webview-ui/src/i18n/locales/it/common.json index e32bf2c712e..20886d126ba 100644 --- a/webview-ui/src/i18n/locales/it/common.json +++ b/webview-ui/src/i18n/locales/it/common.json @@ -20,7 +20,8 @@ "billion_suffix": "b" }, "ui": { - "search_placeholder": "Cerca..." + "search_placeholder": "Cerca...", + "no_results": "Nessun risultato trovato" }, "mermaid": { "loading": "Generazione del diagramma mermaid...", diff --git a/webview-ui/src/i18n/locales/ja/common.json b/webview-ui/src/i18n/locales/ja/common.json index 7c7047dfe8b..a7390de32ab 100644 --- a/webview-ui/src/i18n/locales/ja/common.json +++ b/webview-ui/src/i18n/locales/ja/common.json @@ -20,7 +20,8 @@ "billion_suffix": "b" }, "ui": { - "search_placeholder": "検索..." + "search_placeholder": "検索...", + "no_results": "結果が見つかりません" }, "mermaid": { "loading": "Mermaidダイアグラムを生成中...", diff --git a/webview-ui/src/i18n/locales/ko/common.json b/webview-ui/src/i18n/locales/ko/common.json index 3aef6758c25..2164c656248 100644 --- a/webview-ui/src/i18n/locales/ko/common.json +++ b/webview-ui/src/i18n/locales/ko/common.json @@ -20,7 +20,8 @@ "billion_suffix": "b" }, "ui": { - "search_placeholder": "검색..." + "search_placeholder": "검색...", + "no_results": "결과를 찾을 수 없습니다" }, "mermaid": { "loading": "머메이드 다이어그램 생성 중...", diff --git a/webview-ui/src/i18n/locales/nl/common.json b/webview-ui/src/i18n/locales/nl/common.json index 2db305d20e1..4b72bccb9c5 100644 --- a/webview-ui/src/i18n/locales/nl/common.json +++ b/webview-ui/src/i18n/locales/nl/common.json @@ -20,7 +20,8 @@ "billion_suffix": "mrd" }, "ui": { - "search_placeholder": "Zoeken..." + "search_placeholder": "Zoeken...", + "no_results": "Geen resultaten gevonden" }, "mermaid": { "loading": "Mermaid-diagram genereren...", diff --git a/webview-ui/src/i18n/locales/pl/common.json b/webview-ui/src/i18n/locales/pl/common.json index a0cfc9ccd93..6ec9e6661ad 100644 --- a/webview-ui/src/i18n/locales/pl/common.json +++ b/webview-ui/src/i18n/locales/pl/common.json @@ -20,7 +20,8 @@ "keep": "Zachowaj" }, "ui": { - "search_placeholder": "Szukaj..." + "search_placeholder": "Szukaj...", + "no_results": "Nie znaleziono wyników" }, "mermaid": { "loading": "Generowanie diagramu mermaid...", diff --git a/webview-ui/src/i18n/locales/pt-BR/common.json b/webview-ui/src/i18n/locales/pt-BR/common.json index 3f922852c7e..964ba893f27 100644 --- a/webview-ui/src/i18n/locales/pt-BR/common.json +++ b/webview-ui/src/i18n/locales/pt-BR/common.json @@ -20,7 +20,8 @@ "keep": "Manter" }, "ui": { - "search_placeholder": "Pesquisar..." + "search_placeholder": "Pesquisar...", + "no_results": "Nenhum resultado encontrado" }, "mermaid": { "loading": "Gerando diagrama mermaid...", diff --git a/webview-ui/src/i18n/locales/ru/common.json b/webview-ui/src/i18n/locales/ru/common.json index efdb5e05c60..772b797bba7 100644 --- a/webview-ui/src/i18n/locales/ru/common.json +++ b/webview-ui/src/i18n/locales/ru/common.json @@ -20,7 +20,8 @@ "keep": "Оставить" }, "ui": { - "search_placeholder": "Поиск..." + "search_placeholder": "Поиск...", + "no_results": "Результатов не найдено" }, "mermaid": { "loading": "Создание диаграммы mermaid...", diff --git a/webview-ui/src/i18n/locales/tr/common.json b/webview-ui/src/i18n/locales/tr/common.json index a33564663c4..7bbb6f3d841 100644 --- a/webview-ui/src/i18n/locales/tr/common.json +++ b/webview-ui/src/i18n/locales/tr/common.json @@ -20,7 +20,8 @@ "billion_suffix": "b" }, "ui": { - "search_placeholder": "Ara..." + "search_placeholder": "Ara...", + "no_results": "Sonuç bulunamadı" }, "mermaid": { "loading": "Mermaid diyagramı oluşturuluyor...", diff --git a/webview-ui/src/i18n/locales/vi/common.json b/webview-ui/src/i18n/locales/vi/common.json index b7d531d71fb..2d364824955 100644 --- a/webview-ui/src/i18n/locales/vi/common.json +++ b/webview-ui/src/i18n/locales/vi/common.json @@ -20,7 +20,8 @@ "billion_suffix": "b" }, "ui": { - "search_placeholder": "Tìm kiếm..." + "search_placeholder": "Tìm kiếm...", + "no_results": "Không tìm thấy kết quả nào" }, "mermaid": { "loading": "Đang tạo biểu đồ mermaid...", diff --git a/webview-ui/src/i18n/locales/zh-CN/common.json b/webview-ui/src/i18n/locales/zh-CN/common.json index 5a5ddb0c690..de6d1cd7feb 100644 --- a/webview-ui/src/i18n/locales/zh-CN/common.json +++ b/webview-ui/src/i18n/locales/zh-CN/common.json @@ -20,7 +20,8 @@ "billion_suffix": "b" }, "ui": { - "search_placeholder": "搜索..." + "search_placeholder": "搜索...", + "no_results": "未找到结果" }, "mermaid": { "loading": "生成 Mermaid 图表中...", diff --git a/webview-ui/src/i18n/locales/zh-TW/common.json b/webview-ui/src/i18n/locales/zh-TW/common.json index fa8fc471325..a3949a2a9d0 100644 --- a/webview-ui/src/i18n/locales/zh-TW/common.json +++ b/webview-ui/src/i18n/locales/zh-TW/common.json @@ -20,7 +20,8 @@ "billion_suffix": "b" }, "ui": { - "search_placeholder": "搜尋..." + "search_placeholder": "搜尋...", + "no_results": "找不到結果" }, "mermaid": { "loading": "產生 Mermaid 圖表中...", From 0846b1cf2d27553538aa3a8fbcd4c4ea71ea3f57 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Wed, 23 Jul 2025 21:32:58 -0600 Subject: [PATCH 05/11] fix: revert to original SelectDropdown for API config selector - Reverted back to using the original SelectDropdown component - This preserves the exact search behavior from before the PR - The search functionality now works as expected --- .../src/components/chat/ChatTextArea.tsx | 127 ++---------------- 1 file changed, 9 insertions(+), 118 deletions(-) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 6a6e24c3e2c..6fff345be72 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -19,13 +19,14 @@ import { SearchResult, } from "@src/utils/context-mentions" import { convertToMentionPath } from "@/utils/path-mentions" -import { SelectDropdown, DropdownOptionType, Button, StandardTooltip } from "@/components/ui" +import { StandardTooltip } from "@/components/ui" import Thumbnails from "../common/Thumbnails" import ModeSelector from "./ModeSelector" +import { ApiConfigSelector } from "./ApiConfigSelector" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" -import { VolumeX, Image, WandSparkles, SendHorizontal, Check, Pin } from "lucide-react" +import { VolumeX, Image, WandSparkles, SendHorizontal } from "lucide-react" import { IndexingStatusBadge } from "./IndexingStatusBadge" import { cn } from "@/lib/utils" import { usePromptHistory } from "./hooks/usePromptHistory" @@ -826,118 +827,9 @@ const ChatTextArea = forwardRef( // Helper function to handle API config change const handleApiConfigChange = useCallback((value: string) => { - if (value === "settingsButtonClicked") { - vscode.postMessage({ - type: "loadApiConfiguration", - text: value, - values: { section: "providers" }, - }) - } else { - vscode.postMessage({ type: "loadApiConfigurationById", text: value }) - } + vscode.postMessage({ type: "loadApiConfigurationById", text: value }) }, []) - // Helper function to get API config options - const getApiConfigOptions = useMemo(() => { - const pinnedConfigs = (listApiConfigMeta || []) - .filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id]) - .map((config) => ({ - value: config.id, - label: config.name, - name: config.name, - type: DropdownOptionType.ITEM, - pinned: true, - })) - .sort((a, b) => a.label.localeCompare(b.label)) - - const unpinnedConfigs = (listApiConfigMeta || []) - .filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id]) - .map((config) => ({ - value: config.id, - label: config.name, - name: config.name, - type: DropdownOptionType.ITEM, - pinned: false, - })) - .sort((a, b) => a.label.localeCompare(b.label)) - - return [ - ...pinnedConfigs, - ...(pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 - ? [ - { - value: "sep-1", - label: t("chat:separator"), - type: DropdownOptionType.SEPARATOR, - }, - ] - : []), - ...unpinnedConfigs, - { - value: "sep-2", - label: t("chat:separator"), - type: DropdownOptionType.SEPARATOR, - }, - { - value: "settingsButtonClicked", - label: t("chat:edit"), - type: DropdownOptionType.ACTION, - }, - ] - }, [listApiConfigMeta, pinnedApiConfigs, t]) - - // Helper function to render API config item - const renderApiConfigItem = useCallback( - ({ type, value, label, pinned }: any) => { - if (type !== DropdownOptionType.ITEM) { - return label - } - - const config = listApiConfigMeta?.find((c) => c.id === value) - const isCurrentConfig = config?.name === currentApiConfigName - - return ( -
-
- {label} -
-
-
- -
- - - -
-
- ) - }, - [listApiConfigMeta, currentApiConfigName, t, togglePinnedApiConfig], - ) - // Helper function to render non-edit mode controls const renderNonEditModeControls = () => (
@@ -945,17 +837,16 @@ const ChatTextArea = forwardRef(
{renderModeSelector()}
-
From 543657d6b1ba845d2732f1cc83d1a9509a9ec4ed Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Wed, 23 Jul 2025 21:45:17 -0600 Subject: [PATCH 06/11] fix: add data-testid to ApiConfigSelector for test compatibility --- webview-ui/src/components/chat/ApiConfigSelector.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx index e93579a1a28..96f5a2fb542 100644 --- a/webview-ui/src/components/chat/ApiConfigSelector.tsx +++ b/webview-ui/src/components/chat/ApiConfigSelector.tsx @@ -134,6 +134,7 @@ export const ApiConfigSelector = ({ const triggerContent = ( Date: Wed, 23 Jul 2025 22:09:00 -0600 Subject: [PATCH 07/11] fix: address PR review feedback for ApiConfigSelector - Add ChevronUp icon to trigger button to match ModeSelector pattern - Add comprehensive test coverage for ApiConfigSelector component - Tests cover all functionality including search, pinning, and settings --- .../src/components/chat/ApiConfigSelector.tsx | 3 +- .../chat/__tests__/ApiConfigSelector.spec.tsx | 319 ++++++++++++++++++ 2 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx index 96f5a2fb542..ee8262ce3f1 100644 --- a/webview-ui/src/components/chat/ApiConfigSelector.tsx +++ b/webview-ui/src/components/chat/ApiConfigSelector.tsx @@ -6,7 +6,7 @@ import { IconButton } from "./IconButton" import { useAppTranslation } from "@/i18n/TranslationContext" import { vscode } from "@/utils/vscode" import { Fzf } from "fzf" -import { Check, X, Pin } from "lucide-react" +import { Check, X, Pin, ChevronUp } from "lucide-react" import { Button } from "@/components/ui" interface ApiConfigSelectorProps { @@ -144,6 +144,7 @@ export const ApiConfigSelector = ({ : "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer", triggerClassName, )}> + {displayName} ) diff --git a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx new file mode 100644 index 00000000000..6e169f4e4a3 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx @@ -0,0 +1,319 @@ +import React from "react" +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" +import { describe, test, expect, vi, beforeEach } from "vitest" +import { ApiConfigSelector } from "../ApiConfigSelector" +import { vscode } from "@/utils/vscode" + +// Mock the dependencies +vi.mock("@/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock("@/components/ui/hooks/useRooPortal", () => ({ + useRooPortal: () => document.body, +})) + +// Mock Popover components to be testable +vi.mock("@/components/ui", () => ({ + Popover: ({ children, open }: any) => ( +
+ {children} +
+ ), + PopoverTrigger: ({ children, disabled, ...props }: any) => ( + + ), + PopoverContent: ({ children }: any) =>
{children}
, + StandardTooltip: ({ children }: any) => <>{children}, + Button: ({ children, onClick, ...props }: any) => ( + + ), +})) + +describe("ApiConfigSelector", () => { + const mockOnChange = vi.fn() + const mockTogglePinnedApiConfig = vi.fn() + + const defaultProps = { + value: "config1", + displayName: "Config 1", + onChange: mockOnChange, + listApiConfigMeta: [ + { id: "config1", name: "Config 1" }, + { id: "config2", name: "Config 2" }, + { id: "config3", name: "Config 3" }, + ], + pinnedApiConfigs: { config1: true }, + togglePinnedApiConfig: mockTogglePinnedApiConfig, + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + test("renders correctly with default props", () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + expect(trigger).toBeInTheDocument() + expect(trigger).toHaveTextContent("Config 1") + }) + + test("renders with ChevronUp icon", () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + // Check for the icon by looking for the SVG element + const icon = trigger.querySelector("svg") + expect(icon).toBeInTheDocument() + }) + + test("handles disabled state correctly", () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + expect(trigger).toBeDisabled() + }) + + test("renders with custom title tooltip", () => { + const customTitle = "Custom tooltip text" + render() + + // The component should render with the tooltip wrapper + const trigger = screen.getByTestId("dropdown-trigger") + expect(trigger).toBeInTheDocument() + }) + + test("applies custom trigger className", () => { + const customClass = "custom-trigger-class" + render() + + const trigger = screen.getByTestId("dropdown-trigger") + expect(trigger.className).toContain(customClass) + }) + + test("opens popover when trigger is clicked", () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + // Check if popover content is rendered + const popoverContent = screen.getByTestId("popover-content") + expect(popoverContent).toBeInTheDocument() + }) + + test("renders search input when popover is open", () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + const searchInput = screen.getByPlaceholderText("common:ui.search_placeholder") + expect(searchInput).toBeInTheDocument() + }) + + test("filters configs based on search input", async () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + const searchInput = screen.getByPlaceholderText("common:ui.search_placeholder") + fireEvent.change(searchInput, { target: { value: "Config 2" } }) + + // Wait for the filtering to take effect + await waitFor(() => { + // Config 2 should be visible + expect(screen.getByText("Config 2")).toBeInTheDocument() + // Config 3 should not be visible (assuming exact match filtering) + expect(screen.queryByText("Config 3")).not.toBeInTheDocument() + }) + }) + + test("shows no results message when search has no matches", async () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + const searchInput = screen.getByPlaceholderText("common:ui.search_placeholder") + fireEvent.change(searchInput, { target: { value: "NonExistentConfig" } }) + + await waitFor(() => { + expect(screen.getByText("common:ui.no_results")).toBeInTheDocument() + }) + }) + + test("clears search when X button is clicked", async () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + const searchInput = screen.getByPlaceholderText("common:ui.search_placeholder") as HTMLInputElement + fireEvent.change(searchInput, { target: { value: "test" } }) + + expect(searchInput.value).toBe("test") + + // Find and click the X button + const clearButton = screen.getByTestId("popover-content").querySelector(".cursor-pointer") + if (clearButton) { + fireEvent.click(clearButton) + } + + await waitFor(() => { + expect(searchInput.value).toBe("") + }) + }) + + test("calls onChange when a config is selected", () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + const config2 = screen.getByText("Config 2") + fireEvent.click(config2) + + expect(mockOnChange).toHaveBeenCalledWith("config2") + }) + + test("shows check mark for selected config", () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + // The selected config (config1) should have a check mark + // Use getAllByText since there might be multiple elements with "Config 1" + const config1Elements = screen.getAllByText("Config 1") + // Find the one that's in the dropdown content (not the trigger) + const configInDropdown = config1Elements.find((el) => el.closest('[data-testid="popover-content"]')) + const selectedConfigRow = configInDropdown?.closest("div") + const checkIcon = selectedConfigRow?.querySelector("svg") + expect(checkIcon).toBeInTheDocument() + }) + + test("separates pinned and unpinned configs", () => { + const props = { + ...defaultProps, + pinnedApiConfigs: { config1: true, config3: true }, + } + + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + const content = screen.getByTestId("popover-content") + const configTexts = content.querySelectorAll(".truncate") + + // Pinned configs should appear first + expect(configTexts[0]).toHaveTextContent("Config 1") + expect(configTexts[1]).toHaveTextContent("Config 3") + // Unpinned config should appear after separator + expect(configTexts[2]).toHaveTextContent("Config 2") + }) + + test("toggles pin status when pin button is clicked", () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + // Find the pin button for Config 2 (unpinned) + const config2Row = screen.getByText("Config 2").closest("div") + const pinButton = config2Row?.querySelector("button") + + if (pinButton) { + fireEvent.click(pinButton) + } + + expect(mockTogglePinnedApiConfig).toHaveBeenCalledWith("config2") + expect(vi.mocked(vscode.postMessage)).toHaveBeenCalledWith({ + type: "toggleApiConfigPin", + text: "config2", + }) + }) + + test("opens settings when edit button is clicked", () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + // Find the settings button by its icon class within the popover content + const popoverContent = screen.getByTestId("popover-content") + const settingsButton = popoverContent.querySelector('[aria-label="chat:edit"]') as HTMLElement + expect(settingsButton).toBeInTheDocument() + fireEvent.click(settingsButton) + + expect(vi.mocked(vscode.postMessage)).toHaveBeenCalledWith({ + type: "switchTab", + tab: "settings", + }) + }) + + test("renders bottom bar with title and info icon", () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + // Check for the title + expect(screen.getByText("prompts:apiConfiguration.title")).toBeInTheDocument() + + // Check for the info icon + const infoIcon = screen.getByTestId("popover-content").querySelector(".codicon-info") + expect(infoIcon).toBeInTheDocument() + }) + + test("handles empty config list gracefully", () => { + const props = { + ...defaultProps, + listApiConfigMeta: [], + } + + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + // Should still render the search and bottom bar + expect(screen.getByPlaceholderText("common:ui.search_placeholder")).toBeInTheDocument() + expect(screen.getByText("prompts:apiConfiguration.title")).toBeInTheDocument() + }) + + test("maintains search value when pinning/unpinning", async () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + const searchInput = screen.getByPlaceholderText("common:ui.search_placeholder") as HTMLInputElement + fireEvent.change(searchInput, { target: { value: "Config" } }) + + // Pin a config + const config2Row = screen.getByText("Config 2").closest("div") + const pinButton = config2Row?.querySelector("button") + if (pinButton) { + fireEvent.click(pinButton) + } + + // Search value should be maintained + expect(searchInput.value).toBe("Config") + }) +}) From 955ef7e2369421288aa5e86fbc6d4ea580da1768 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Sat, 26 Jul 2025 15:39:13 -0600 Subject: [PATCH 08/11] feat: conditionally show search bar in ApiConfigSelector only when > 6 items - Show search bar only when there are more than 6 API configurations - Display info blurb in place of search bar when 6 or fewer configs - Hide info icon when showing the info blurb to avoid duplication - Update tests to handle conditional rendering of search functionality --- .../src/components/chat/ApiConfigSelector.tsx | 54 ++++---- .../chat/__tests__/ApiConfigSelector.spec.tsx | 119 ++++++++++++++++-- 2 files changed, 141 insertions(+), 32 deletions(-) diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx index ee8262ce3f1..a9b53e7b8ac 100644 --- a/webview-ui/src/components/chat/ApiConfigSelector.tsx +++ b/webview-ui/src/components/chat/ApiConfigSelector.tsx @@ -158,25 +158,33 @@ export const ApiConfigSelector = ({ container={portalContainer} className="p-0 overflow-hidden w-[300px]">
- {/* Search input */} -
- setSearchValue(e.target.value)} - 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" - autoFocus - /> - {searchValue.length > 0 && ( -
- setSearchValue("")} - /> -
- )} -
+ {/* Search input or info blurb */} + {listApiConfigMeta.length > 6 ? ( +
+ setSearchValue(e.target.value)} + 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" + autoFocus + /> + {searchValue.length > 0 && ( +
+ setSearchValue("")} + /> +
+ )} +
+ ) : ( +
+

+ {t("prompts:apiConfiguration.select")} +

+
+ )} {/* Config list */}
@@ -212,9 +220,11 @@ export const ApiConfigSelector = ({ {/* Info icon and title on the right with matching spacing */}
- - - + {listApiConfigMeta.length > 6 && ( + + + + )}

{t("prompts:apiConfiguration.title")}

diff --git a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx index 6e169f4e4a3..c7416fec22d 100644 --- a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx @@ -115,8 +115,20 @@ describe("ApiConfigSelector", () => { expect(popoverContent).toBeInTheDocument() }) - test("renders search input when popover is open", () => { - render() + test("renders search input when popover is open and more than 6 configs", () => { + const props = { + ...defaultProps, + listApiConfigMeta: [ + { id: "config1", name: "Config 1" }, + { id: "config2", name: "Config 2" }, + { id: "config3", name: "Config 3" }, + { id: "config4", name: "Config 4" }, + { id: "config5", name: "Config 5" }, + { id: "config6", name: "Config 6" }, + { id: "config7", name: "Config 7" }, + ], + } + render() const trigger = screen.getByTestId("dropdown-trigger") fireEvent.click(trigger) @@ -125,12 +137,36 @@ describe("ApiConfigSelector", () => { expect(searchInput).toBeInTheDocument() }) - test("filters configs based on search input", async () => { + test("renders info blurb instead of search when 6 or fewer configs", () => { render() const trigger = screen.getByTestId("dropdown-trigger") fireEvent.click(trigger) + // Should not have search input + expect(screen.queryByPlaceholderText("common:ui.search_placeholder")).not.toBeInTheDocument() + // Should have info blurb + expect(screen.getByText("prompts:apiConfiguration.select")).toBeInTheDocument() + }) + + test("filters configs based on search input", async () => { + const props = { + ...defaultProps, + listApiConfigMeta: [ + { id: "config1", name: "Config 1" }, + { id: "config2", name: "Config 2" }, + { id: "config3", name: "Config 3" }, + { id: "config4", name: "Config 4" }, + { id: "config5", name: "Config 5" }, + { id: "config6", name: "Config 6" }, + { id: "config7", name: "Config 7" }, + ], + } + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + const searchInput = screen.getByPlaceholderText("common:ui.search_placeholder") fireEvent.change(searchInput, { target: { value: "Config 2" } }) @@ -144,7 +180,19 @@ describe("ApiConfigSelector", () => { }) test("shows no results message when search has no matches", async () => { - render() + const props = { + ...defaultProps, + listApiConfigMeta: [ + { id: "config1", name: "Config 1" }, + { id: "config2", name: "Config 2" }, + { id: "config3", name: "Config 3" }, + { id: "config4", name: "Config 4" }, + { id: "config5", name: "Config 5" }, + { id: "config6", name: "Config 6" }, + { id: "config7", name: "Config 7" }, + ], + } + render() const trigger = screen.getByTestId("dropdown-trigger") fireEvent.click(trigger) @@ -158,7 +206,19 @@ describe("ApiConfigSelector", () => { }) test("clears search when X button is clicked", async () => { - render() + const props = { + ...defaultProps, + listApiConfigMeta: [ + { id: "config1", name: "Config 1" }, + { id: "config2", name: "Config 2" }, + { id: "config3", name: "Config 3" }, + { id: "config4", name: "Config 4" }, + { id: "config5", name: "Config 5" }, + { id: "config6", name: "Config 6" }, + { id: "config7", name: "Config 7" }, + ], + } + render() const trigger = screen.getByTestId("dropdown-trigger") fireEvent.click(trigger) @@ -267,8 +327,20 @@ describe("ApiConfigSelector", () => { }) }) - test("renders bottom bar with title and info icon", () => { - render() + test("renders bottom bar with title and info icon when more than 6 configs", () => { + const props = { + ...defaultProps, + listApiConfigMeta: [ + { id: "config1", name: "Config 1" }, + { id: "config2", name: "Config 2" }, + { id: "config3", name: "Config 3" }, + { id: "config4", name: "Config 4" }, + { id: "config5", name: "Config 5" }, + { id: "config6", name: "Config 6" }, + { id: "config7", name: "Config 7" }, + ], + } + render() const trigger = screen.getByTestId("dropdown-trigger") fireEvent.click(trigger) @@ -281,6 +353,20 @@ describe("ApiConfigSelector", () => { expect(infoIcon).toBeInTheDocument() }) + test("renders bottom bar with title but no info icon when 6 or fewer configs", () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + // Check for the title + expect(screen.getByText("prompts:apiConfiguration.title")).toBeInTheDocument() + + // Check that info icon is not present + const infoIcon = screen.getByTestId("popover-content").querySelector(".codicon-info") + expect(infoIcon).not.toBeInTheDocument() + }) + test("handles empty config list gracefully", () => { const props = { ...defaultProps, @@ -292,13 +378,26 @@ describe("ApiConfigSelector", () => { const trigger = screen.getByTestId("dropdown-trigger") fireEvent.click(trigger) - // Should still render the search and bottom bar - expect(screen.getByPlaceholderText("common:ui.search_placeholder")).toBeInTheDocument() + // Should render info blurb instead of search for empty list + expect(screen.queryByPlaceholderText("common:ui.search_placeholder")).not.toBeInTheDocument() + expect(screen.getByText("prompts:apiConfiguration.select")).toBeInTheDocument() expect(screen.getByText("prompts:apiConfiguration.title")).toBeInTheDocument() }) test("maintains search value when pinning/unpinning", async () => { - render() + const props = { + ...defaultProps, + listApiConfigMeta: [ + { id: "config1", name: "Config 1" }, + { id: "config2", name: "Config 2" }, + { id: "config3", name: "Config 3" }, + { id: "config4", name: "Config 4" }, + { id: "config5", name: "Config 5" }, + { id: "config6", name: "Config 6" }, + { id: "config7", name: "Config 7" }, + ], + } + render() const trigger = screen.getByTestId("dropdown-trigger") fireEvent.click(trigger) From 202fb59a0e2ede0bc64f7566d525ea642198d99d Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Sat, 26 Jul 2025 15:51:52 -0600 Subject: [PATCH 09/11] fix: prevent auto-focus on pin buttons in ApiConfigSelector - Add tabIndex={-1} to pin buttons to prevent them from receiving focus automatically - This fixes the issue where focus would jump to the first pin button when opening the dropdown with 6 or fewer items --- webview-ui/src/components/chat/ApiConfigSelector.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx index a9b53e7b8ac..b769f3f3666 100644 --- a/webview-ui/src/components/chat/ApiConfigSelector.tsx +++ b/webview-ui/src/components/chat/ApiConfigSelector.tsx @@ -109,6 +109,7 @@ export const ApiConfigSelector = ({
@@ -145,7 +144,12 @@ export const ApiConfigSelector = ({ : "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer", triggerClassName, )}> - + {displayName} ) @@ -172,8 +176,8 @@ export const ApiConfigSelector = ({ /> {searchValue.length > 0 && (
- setSearchValue("")} />
From 4802da7306ff794663c34f3217cdd0bf74cadaba Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Sat, 26 Jul 2025 19:31:35 -0500 Subject: [PATCH 11/11] fix: update tests to match codicon implementation instead of SVG --- .../components/chat/__tests__/ApiConfigSelector.spec.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx index c7416fec22d..934d14cc7bd 100644 --- a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx @@ -75,8 +75,8 @@ describe("ApiConfigSelector", () => { render() const trigger = screen.getByTestId("dropdown-trigger") - // Check for the icon by looking for the SVG element - const icon = trigger.querySelector("svg") + // Check for the icon by looking for the codicon span element + const icon = trigger.querySelector(".codicon-chevron-up") expect(icon).toBeInTheDocument() }) @@ -263,7 +263,7 @@ describe("ApiConfigSelector", () => { // Find the one that's in the dropdown content (not the trigger) const configInDropdown = config1Elements.find((el) => el.closest('[data-testid="popover-content"]')) const selectedConfigRow = configInDropdown?.closest("div") - const checkIcon = selectedConfigRow?.querySelector("svg") + const checkIcon = selectedConfigRow?.querySelector(".codicon-check") expect(checkIcon).toBeInTheDocument() })