Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fix-model-dropdown-scrollbar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"kilo-code": patch
---

Fix double scrollbar in model dropdown and make search box sticky

The model selection dropdown previously showed two scrollbars - one on the outer container and one on the inner items list. Additionally, the search box would scroll out of view when browsing through the model list. This fix restructures the dropdown to use a single scrollbar on the items container only, while keeping the search input sticky at the top for better usability.
2 changes: 1 addition & 1 deletion webview-ui/src/components/kilocode/chat/ModelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export const ModelSelector = ({
title={t("chat:selectApiConfig")}
options={options}
onChange={onChange}
contentClassName="max-h-[300px] overflow-y-auto"
contentClassName="max-h-[300px]"
triggerClassName={cn(
"w-full text-ellipsis overflow-hidden p-0",
"bg-transparent border-transparent hover:bg-transparent hover:border-transparent",
Expand Down
231 changes: 115 additions & 116 deletions webview-ui/src/components/ui/select-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,128 +272,127 @@ export const SelectDropdown = React.memo(
align={align}
sideOffset={sideOffset}
container={portalContainer}
className={cn("p-0 overflow-hidden", contentClassName)}>
<div className="flex flex-col w-full">
{/* Search input */}
{!disableSearch && (
<div className="relative p-2 border-b border-vscode-dropdown-border">
<input
aria-label="Search"
ref={searchInputRef}
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"
/>
{searchValue.length > 0 && (
<div className="absolute right-4 top-0 bottom-0 flex items-center justify-center">
<X
className="text-vscode-input-foreground opacity-50 hover:opacity-100 size-4 p-0.5 cursor-pointer"
onClick={onClearSearch}
/>
</div>
)}
</div>
)}
className={cn("p-0 flex flex-col", contentClassName)}>
{/* Search input - Sticky at top */}
{/* kilocode_change: made search sticky and restructured scroll containers */}
{!disableSearch && (
<div className="relative p-2 border-b border-vscode-dropdown-border bg-vscode-dropdown-background sticky top-0 z-10 flex-shrink-0">
<input
aria-label="Search"
ref={searchInputRef}
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"
/>
{searchValue.length > 0 && (
<div className="absolute right-4 top-0 bottom-0 flex items-center justify-center">
<X
className="text-vscode-input-foreground opacity-50 hover:opacity-100 size-4 p-0.5 cursor-pointer"
onClick={onClearSearch}
/>
</div>
)}
</div>
)}

{/* Dropdown items - Scrollable container */}
{/* kilocode_change: single scrollbar for items only */}
<div className="overflow-y-auto flex-1">
{groupedOptions.length === 0 && searchValue ? (
<div className="py-2 px-3 text-sm text-vscode-foreground/70">No results found</div>
) : (
<div className="py-1">
{groupedOptions.map((option, index) => {
// Memoize rendering of each item type for better performance
if (option.type === DropdownOptionType.SEPARATOR) {
return (
<div
key={`sep-${index}`}
className="mx-1 my-1 h-px bg-vscode-dropdown-foreground/10"
data-testid="dropdown-separator"
/>
)
}

{/* Dropdown items - Use windowing for large lists */}
{/* kilocode_change: different max height: max-h-82 */}
<div className="max-h-82 overflow-y-auto">
{groupedOptions.length === 0 && searchValue ? (
<div className="py-2 px-3 text-sm text-vscode-foreground/70">No results found</div>
) : (
<div className="py-1">
{groupedOptions.map((option, index) => {
// Memoize rendering of each item type for better performance
if (option.type === DropdownOptionType.SEPARATOR) {
return (
<div
key={`sep-${index}`}
className="mx-1 my-1 h-px bg-vscode-dropdown-foreground/10"
data-testid="dropdown-separator"
/>
)
}

// kilocode_change start: render LABEL type as section header
if (option.type === DropdownOptionType.LABEL) {
return (
<div
key={`label-${index}`}
className="px-3 py-1.5 text-xs font-medium text-vscode-descriptionForeground uppercase tracking-wide"
data-testid="dropdown-label">
{option.label}
</div>
)
}
// kilocode_change end

if (
option.type === DropdownOptionType.SHORTCUT ||
(option.disabled && shortcutText && option.label.includes(shortcutText))
) {
return (
<div
key={`shortcut-${index}`}
className="px-3 py-1.5 text-sm opacity-50">
{option.label}
</div>
)
}

// Use stable keys for better reconciliation
const itemKey = `item-${option.value || option.label || index}`
// kilocode_change start: render LABEL type as section header
if (option.type === DropdownOptionType.LABEL) {
return (
<div
key={`label-${index}`}
className="px-3 py-1.5 text-xs font-medium text-vscode-descriptionForeground uppercase tracking-wide"
data-testid="dropdown-label">
{option.label}
</div>
)
}
// kilocode_change end

if (
option.type === DropdownOptionType.SHORTCUT ||
(option.disabled && shortcutText && option.label.includes(shortcutText))
) {
return (
<div
key={itemKey}
onClick={() => !option.disabled && handleSelect(option.value)}
className={cn(
"text-sm cursor-pointer flex items-center", // kilocode_change
option.disabled
? "opacity-50 cursor-not-allowed"
: "hover:bg-vscode-list-hoverBackground",
option.value === value
? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
: "",
itemClassName,
)}
data-testid="dropdown-item">
{renderItem ? (
renderItem(option)
) : (
<>
{/* kilocode_change start */}
<div className="flex items-center flex-1 py-1.5 px-3 hover:bg-vscode-list-hoverBackground">
<span
slot="start"
style={{ fontSize: "14px" }}
className={cn(
"codicon opacity-80 mr-2",
option.codicon,
)}
/>
<div className="flex-1">
<div>{option.label}</div>
{option.description && (
<div className="text-[11px] opacity-50 mt-0.5">
{option.description}
</div>
)}
</div>
{/* kilocode_change end */}
{option.value === value && (
<Check className="ml-auto size-4 p-0.5" />
)}
</div>
</>
)}
key={`shortcut-${index}`}
className="px-3 py-1.5 text-sm opacity-50">
{option.label}
</div>
)
})}
</div>
)}
</div>
}

// Use stable keys for better reconciliation
const itemKey = `item-${option.value || option.label || index}`

return (
<div
key={itemKey}
onClick={() => !option.disabled && handleSelect(option.value)}
className={cn(
"text-sm cursor-pointer flex items-center", // kilocode_change
option.disabled
? "opacity-50 cursor-not-allowed"
: "hover:bg-vscode-list-hoverBackground",
option.value === value
? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
: "",
itemClassName,
)}
data-testid="dropdown-item">
{renderItem ? (
renderItem(option)
) : (
<>
{/* kilocode_change start */}
<div className="flex items-center flex-1 py-1.5 px-3 hover:bg-vscode-list-hoverBackground">
<span
slot="start"
style={{ fontSize: "14px" }}
className={cn(
"codicon opacity-80 mr-2",
option.codicon,
)}
/>
<div className="flex-1">
<div>{option.label}</div>
{option.description && (
<div className="text-[11px] opacity-50 mt-0.5">
{option.description}
</div>
)}
</div>
{/* kilocode_change end */}
{option.value === value && (
<Check className="ml-auto size-4 p-0.5" />
)}
</div>
</>
)}
</div>
)
})}
</div>
)}
</div>
</PopoverContent>
</Popover>
Expand Down