diff --git a/apps/web/components/model-config-selector.test.tsx b/apps/web/components/model-config-selector.test.tsx index 196896fcad..7c88169378 100644 --- a/apps/web/components/model-config-selector.test.tsx +++ b/apps/web/components/model-config-selector.test.tsx @@ -102,6 +102,126 @@ describe("ModelConfigSelector", () => { }); }); +describe("ModelConfigSelector loading behavior", () => { + it("keeps the picker open after selecting a model without existing dependent options", () => { + const onModelChange = vi.fn(); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: modelSettingsButtonName })); + fireEvent.click(screen.getByRole("option", { name: /Model B/ })); + + expect(onModelChange).toHaveBeenCalledWith("model-b"); + expect(screen.getByRole("option", { name: /Model B/ })).toBeTruthy(); + }); + + it("replaces dependent options with a loading row until the snapshot resolves", () => { + const { rerender } = render( + {}} + configOptions={[ + { + type: "select", + id: "effort", + name: "Effort", + currentValue: "medium", + options: [{ value: "medium", name: "Medium" }], + }, + ]} + />, + ); + + fireEvent.click(screen.getByRole("button", { name: modelSettingsButtonName })); + expect(screen.getByTestId(effortTriggerTestId)).toBeTruthy(); + + rerender( + {}} + configOptions={[ + { + type: "select", + id: "effort", + name: "Effort", + currentValue: "medium", + options: [{ value: "medium", name: "Medium" }], + }, + ]} + configOptionsLoading + keepOpenOnModelChange + />, + ); + + const loadingRow = screen.getByTestId("model-config-options-loading"); + expect(loadingRow.getAttribute("role")).toBe("status"); + expect(screen.getByRole("status", { name: "Loading model options…" })).toBe(loadingRow); + expect(screen.queryByTestId(effortTriggerTestId)).toBeNull(); + const selectedModelRow = screen.getByTestId("model-config-selected-row"); + expect(selectedModelRow.querySelector("svg.tabler-icon-loader")).toBeTruthy(); + expect(selectedModelRow.querySelector("svg.tabler-icon-check.absolute")).toBeNull(); + + rerender( + {}} + configOptions={[ + { + type: "select", + id: "effort", + name: "Effort", + currentValue: "max", + options: [{ value: "max", name: "Max" }], + }, + ]} + keepOpenOnModelChange + />, + ); + + expect(screen.queryByTestId("model-config-options-loading")).toBeNull(); + expect(selectedModelRow.querySelector("svg.tabler-icon-loader")).toBeNull(); + expect(selectedModelRow.querySelector("svg.tabler-icon-check.absolute")).toBeTruthy(); + expect(screen.getByTestId(effortTriggerTestId).textContent).toContain("Max"); + }); +}); + +describe("ModelConfigSelector loading guard", () => { + it("keeps the picker open when options are loading without the explicit open flag", () => { + const onModelChange = vi.fn(); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: modelSettingsButtonName })); + fireEvent.click(screen.getByRole("option", { name: /Model B/ })); + + expect(onModelChange).toHaveBeenCalledWith("model-b"); + expect(screen.getByTestId("model-config-options-loading")).toBeTruthy(); + }); +}); + describe("ModelConfigSelector filtering", () => { it("deduplicates repeated model ids from provider config", () => { const modelConfig: SelectConfigOption = { diff --git a/apps/web/components/model-config-selector.tsx b/apps/web/components/model-config-selector.tsx index c8310c8acf..d88471cffe 100644 --- a/apps/web/components/model-config-selector.tsx +++ b/apps/web/components/model-config-selector.tsx @@ -19,6 +19,7 @@ import { import { Popover, PopoverContent, PopoverTrigger } from "@kandev/ui/popover"; import { ScrollArea } from "@kandev/ui/scroll-area"; import { Separator } from "@kandev/ui/separator"; +import { Spinner } from "@kandev/ui/spinner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; export type ModelSelectorOption = { @@ -186,10 +187,12 @@ function triggerDetails( function ModelRow({ model, selected, + loading, onSelect, }: { model: ModelSelectorOption; selected: boolean; + loading: boolean; onSelect: (value: string) => void; }) { const item = ( @@ -198,6 +201,7 @@ function ModelRow({ keywords={[model.name, model.description ?? "", model.id]} onSelect={() => !model.disabled && onSelect(model.id)} disabled={model.disabled} + data-testid={selected ? "model-config-selected-row" : undefined} className={cn("relative pr-7", model.disabled && "opacity-40 cursor-not-allowed")} >
@@ -213,16 +217,17 @@ function ModelRow({ {model.usageMultiplier} )}
- + {selected && loading ? ( +