diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx
index 786ad01dff6d..4396019a2d27 100644
--- a/webview-ui/src/components/chat/ApiConfigSelector.tsx
+++ b/webview-ui/src/components/chat/ApiConfigSelector.tsx
@@ -193,27 +193,31 @@ export const ApiConfigSelector = ({
)}
- {/* 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))}
-
- )}
-
+ {/* Config list - single scroll container */}
+ {filteredConfigs.length === 0 && searchValue ? (
+
+ {/* Pinned configs - sticky header */}
+ {pinnedConfigs.length > 0 && (
+
0 && "border-b border-vscode-dropdown-foreground/10",
+ )}
+ aria-label="Pinned configurations">
+ {pinnedConfigs.map((config) => renderConfigItem(config, true))}
+
+ )}
+
+ {/* Unpinned configs */}
+ {unpinnedConfigs.length > 0 && (
+
+ {unpinnedConfigs.map((config) => renderConfigItem(config, false))}
+
+ )}
+
+ )}
{/* Bottom bar with buttons on left and title on right */}
diff --git a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx
index 5b25b3bef46b..ff1b95f94999 100644
--- a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx
@@ -435,4 +435,104 @@ describe("ApiConfigSelector", () => {
// Search value should be maintained
expect(searchInput.value).toBe("Config")
})
+
+ test("pinned configs remain fixed at top while unpinned configs scroll", () => {
+ // Create a list with many configs to test scrolling
+ const manyConfigs = Array.from({ length: 15 }, (_, i) => ({
+ id: `config${i + 1}`,
+ name: `Config ${i + 1}`,
+ modelId: `model-${i + 1}`,
+ }))
+
+ const props = {
+ ...defaultProps,
+ listApiConfigMeta: manyConfigs,
+ pinnedApiConfigs: {
+ config1: true,
+ config2: true,
+ config3: true,
+ },
+ }
+
+ render(
)
+
+ const trigger = screen.getByTestId("dropdown-trigger")
+ fireEvent.click(trigger)
+
+ const popoverContent = screen.getByTestId("popover-content")
+
+ // Should have a single scroll container with max-h-[300px] and overflow-y-auto
+ const scrollContainer = popoverContent.querySelector(".max-h-\\[300px\\].overflow-y-auto")
+ expect(scrollContainer).toBeInTheDocument()
+
+ // Check for pinned configs sticky header
+ const pinnedStickyHeader = scrollContainer?.querySelector(".sticky.top-0.z-10.bg-vscode-dropdown-background")
+ expect(pinnedStickyHeader).toBeInTheDocument()
+ expect(pinnedStickyHeader).toHaveAttribute("aria-label", "Pinned configurations")
+
+ // Check for Config 1, 2, 3 being visible in the sticky header (pinned)
+ expect(screen.getAllByText("Config 1").length).toBeGreaterThan(0)
+ expect(screen.getAllByText("Config 2").length).toBeGreaterThan(0)
+ expect(screen.getAllByText("Config 3").length).toBeGreaterThan(0)
+
+ // Verify pinned container contains the pinned configs
+ if (pinnedStickyHeader) {
+ const elements = pinnedStickyHeader.querySelectorAll(".flex-shrink-0")
+ const pinnedConfigTexts = Array.from(elements)
+ .map((el) => (el as Element).textContent)
+ .filter((text) => text?.startsWith("Config"))
+
+ expect(pinnedConfigTexts).toContain("Config 1")
+ expect(pinnedConfigTexts).toContain("Config 2")
+ expect(pinnedConfigTexts).toContain("Config 3")
+ }
+
+ // Check for unpinned configs section
+ const unpinnedSection = scrollContainer?.querySelector('[aria-label="All configurations"]')
+ expect(unpinnedSection).toBeInTheDocument()
+
+ // Verify separator exists as border on pinned section when unpinned configs exist
+ expect(pinnedStickyHeader).toHaveClass("border-b")
+ })
+
+ test("displays all configs in scrollable container when no configs are pinned", () => {
+ const manyConfigs = Array.from({ length: 10 }, (_, i) => ({
+ id: `config${i + 1}`,
+ name: `Config ${i + 1}`,
+ modelId: `model-${i + 1}`,
+ }))
+
+ const props = {
+ ...defaultProps,
+ listApiConfigMeta: manyConfigs,
+ pinnedApiConfigs: {}, // No pinned configs
+ }
+
+ render(
)
+
+ const trigger = screen.getByTestId("dropdown-trigger")
+ fireEvent.click(trigger)
+
+ const popoverContent = screen.getByTestId("popover-content")
+
+ // Should have a single scroll container with max-h-[300px] and overflow-y-auto
+ const scrollContainer = popoverContent.querySelector(".max-h-\\[300px\\].overflow-y-auto")
+ expect(scrollContainer).toBeInTheDocument()
+
+ // No pinned section should exist when no configs are pinned
+ const pinnedSection = scrollContainer?.querySelector(".sticky.top-0")
+ expect(pinnedSection).not.toBeInTheDocument()
+
+ // Should have unpinned configs section with all configs
+ const unpinnedSection = scrollContainer?.querySelector('[aria-label="All configurations"]')
+ expect(unpinnedSection).toBeInTheDocument()
+
+ // All configs should be in the unpinned section
+ const allConfigRows = unpinnedSection?.querySelectorAll(".group")
+ expect(allConfigRows?.length).toBe(10)
+
+ // No separator should exist when no pinned configs (no sticky header exists)
+ const stickyHeader = scrollContainer?.querySelector(".sticky.top-0")
+ expect(stickyHeader).not.toBeInTheDocument()
+ })
})