-
Notifications
You must be signed in to change notification settings - Fork 2.7k
feat: sync API config selector style with mode selector from PR #6140 #6148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
8318d3e
feat: sync API config selector style with mode selector from PR #6140
hannesrudolph af1d180
fix: add missing no_results translation to common namespace
hannesrudolph e67eb5d
fix: restore search functionality to match original SelectDropdown be…
hannesrudolph a1d93ff
revert: restore original SelectDropdown for API config selector
hannesrudolph 0846b1c
fix: revert to original SelectDropdown for API config selector
hannesrudolph 543657d
fix: add data-testid to ApiConfigSelector for test compatibility
hannesrudolph 94f7001
fix: address PR review feedback for ApiConfigSelector
hannesrudolph 955ef7e
feat: conditionally show search bar in ApiConfigSelector only when > …
hannesrudolph 202fb59
fix: prevent auto-focus on pin buttons in ApiConfigSelector
hannesrudolph 3c24b62
fix: address PR review feedback
daniel-lxs 4802da7
fix: update tests to match codicon implementation instead of SVG
daniel-lxs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| 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 { 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<string, boolean> | ||
| 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 ( | ||
| <div | ||
| key={config.id} | ||
| onClick={() => 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", | ||
| )}> | ||
| <span className="flex-1 truncate">{config.name}</span> | ||
| <div className="flex items-center gap-1"> | ||
| {isCurrentConfig && ( | ||
| <div className="size-5 p-1 flex items-center justify-center"> | ||
| <span className="codicon codicon-check text-xs" /> | ||
| </div> | ||
| )} | ||
| <StandardTooltip content={isPinned ? t("chat:unpin") : t("chat:pin")}> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| tabIndex={-1} | ||
| onClick={(e) => { | ||
| e.stopPropagation() | ||
| togglePinnedApiConfig(config.id) | ||
| vscode.postMessage({ | ||
| type: "toggleApiConfigPin", | ||
| text: config.id, | ||
| }) | ||
| }} | ||
| className={cn("size-5 flex items-center justify-center", { | ||
| "opacity-0 group-hover:opacity-100": !isPinned && !isCurrentConfig, | ||
| "bg-accent opacity-100": isPinned, | ||
| })}> | ||
| <span className="codicon codicon-pin text-xs opacity-50" /> | ||
| </Button> | ||
| </StandardTooltip> | ||
| </div> | ||
| </div> | ||
| ) | ||
| }, | ||
| [value, handleSelect, t, togglePinnedApiConfig], | ||
| ) | ||
|
|
||
| const triggerContent = ( | ||
| <PopoverTrigger | ||
| disabled={disabled} | ||
| data-testid="dropdown-trigger" | ||
| className={cn( | ||
| "w-full min-w-0 max-w-full inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs", | ||
| "bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground", | ||
| "transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset", | ||
| disabled | ||
| ? "opacity-50 cursor-not-allowed" | ||
| : "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer", | ||
| triggerClassName, | ||
| )}> | ||
| <span | ||
| className={cn( | ||
| "codicon codicon-chevron-up pointer-events-none opacity-80 flex-shrink-0 text-xs transition-transform duration-200", | ||
| open && "rotate-180", | ||
| )} | ||
| /> | ||
| <span className="truncate">{displayName}</span> | ||
| </PopoverTrigger> | ||
| ) | ||
|
|
||
| return ( | ||
| <Popover open={open} onOpenChange={setOpen}> | ||
| {title ? <StandardTooltip content={title}>{triggerContent}</StandardTooltip> : triggerContent} | ||
| <PopoverContent | ||
| align="start" | ||
| sideOffset={4} | ||
| container={portalContainer} | ||
| className="p-0 overflow-hidden w-[300px]"> | ||
| <div className="flex flex-col w-full"> | ||
| {/* Search input or info blurb */} | ||
| {listApiConfigMeta.length > 6 ? ( | ||
| <div className="relative p-2 border-b border-vscode-dropdown-border"> | ||
| <input | ||
| aria-label={t("common:ui.search_placeholder")} | ||
| value={searchValue} | ||
| onChange={(e) => 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 && ( | ||
| <div className="absolute right-4 top-0 bottom-0 flex items-center justify-center"> | ||
| <span | ||
| className="codicon codicon-close text-vscode-input-foreground opacity-50 hover:opacity-100 text-xs cursor-pointer" | ||
| onClick={() => setSearchValue("")} | ||
| /> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ) : ( | ||
| <div className="p-3 border-b border-vscode-dropdown-border"> | ||
| <p className="text-xs text-vscode-descriptionForeground m-0"> | ||
| {t("prompts:apiConfiguration.select")} | ||
| </p> | ||
| </div> | ||
| )} | ||
|
|
||
| {/* Config list */} | ||
| <div className="max-h-[300px] overflow-y-auto"> | ||
| {filteredConfigs.length === 0 && searchValue ? ( | ||
| <div className="py-2 px-3 text-sm text-vscode-foreground/70"> | ||
| {t("common:ui.no_results")} | ||
| </div> | ||
| ) : ( | ||
| <div className="py-1"> | ||
| {/* Pinned configs */} | ||
| {pinnedConfigs.map((config) => renderConfigItem(config, true))} | ||
|
|
||
| {/* Separator between pinned and unpinned */} | ||
| {pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 && ( | ||
| <div className="mx-1 my-1 h-px bg-vscode-dropdown-foreground/10" /> | ||
| )} | ||
|
|
||
| {/* Unpinned configs */} | ||
| {unpinnedConfigs.map((config) => renderConfigItem(config, false))} | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Bottom bar with buttons on left and title on right */} | ||
| <div className="flex flex-row items-center justify-between p-2 border-t border-vscode-dropdown-border"> | ||
| <div className="flex flex-row gap-1"> | ||
| <IconButton | ||
| iconClass="codicon-settings-gear" | ||
| title={t("chat:edit")} | ||
| onClick={handleEditClick} | ||
| /> | ||
| </div> | ||
|
|
||
| {/* Info icon and title on the right with matching spacing */} | ||
| <div className="flex items-center gap-1 pr-1"> | ||
| {listApiConfigMeta.length > 6 && ( | ||
| <StandardTooltip content={t("prompts:apiConfiguration.select")}> | ||
| <span className="codicon codicon-info text-xs text-vscode-descriptionForeground opacity-70 hover:opacity-100 cursor-help" /> | ||
| </StandardTooltip> | ||
| )} | ||
| <h4 className="m-0 font-medium text-sm text-vscode-descriptionForeground"> | ||
| {t("prompts:apiConfiguration.title")} | ||
| </h4> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </PopoverContent> | ||
| </Popover> | ||
| ) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.