From 8578b8ac04d3c25be7c0e108ca6541cc8f008dcd Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Wed, 7 Jan 2026 01:26:29 -0500 Subject: [PATCH 01/10] Initial commit of section headers for models --- .../kilocode/chat/ModelSelector.tsx | 76 ++++++++-- .../chat/__tests__/ModelSelector.spec.tsx | 6 +- .../src/components/settings/ModelPicker.tsx | 91 +++++++----- .../ui/__tests__/select-dropdown.spec.tsx | 45 ++++++ .../__tests__/usePreferredModels.spec.ts | 132 ++++++++++++++++++ .../ui/hooks/kilocode/usePreferredModels.ts | 75 ++++++---- .../src/components/ui/select-dropdown.tsx | 78 +++++++++-- webview-ui/src/i18n/locales/en/settings.json | 4 +- 8 files changed, 418 insertions(+), 89 deletions(-) create mode 100644 webview-ui/src/components/ui/hooks/kilocode/__tests__/usePreferredModels.spec.ts diff --git a/webview-ui/src/components/kilocode/chat/ModelSelector.tsx b/webview-ui/src/components/kilocode/chat/ModelSelector.tsx index 1c08983c82d..37f6fd1fd9a 100644 --- a/webview-ui/src/components/kilocode/chat/ModelSelector.tsx +++ b/webview-ui/src/components/kilocode/chat/ModelSelector.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react" -import { SelectDropdown, DropdownOptionType } from "@/components/ui" +import { SelectDropdown, DropdownOptionType, type DropdownOption } from "@/components/ui" import { OPENROUTER_DEFAULT_PROVIDER_NAME, type ProviderSettings } from "@roo-code/types" import { vscode } from "@src/utils/vscode" import { useAppTranslation } from "@src/i18n/TranslationContext" @@ -7,7 +7,7 @@ import { cn } from "@src/lib/utils" import { prettyModelName } from "../../../utils/prettyModelName" import { useProviderModels } from "../hooks/useProviderModels" import { getModelIdKey, getSelectedModelId } from "../hooks/useSelectedModel" -import { usePreferredModels } from "@/components/ui/hooks/kilocode/usePreferredModels" +import { useGroupedModelIds } from "@/components/ui/hooks/kilocode/usePreferredModels" interface ModelSelectorProps { currentApiConfigName?: string @@ -32,15 +32,69 @@ export const ModelSelector = ({ const modelIdKey = getModelIdKey({ provider }) const isAutocomplete = apiConfiguration.profileType === "autocomplete" - const modelsIds = usePreferredModels(providerModels) + const { preferredModelIds, restModelIds, hasPreferred } = useGroupedModelIds(providerModels) const options = useMemo(() => { - const missingModelIds = modelsIds.indexOf(selectedModelId) >= 0 ? [] : [selectedModelId] - return missingModelIds.concat(modelsIds).map((modelId) => ({ - value: modelId, - label: providerModels[modelId]?.displayName ?? prettyModelName(modelId), - type: DropdownOptionType.ITEM, - })) - }, [modelsIds, providerModels, selectedModelId]) + const result: DropdownOption[] = [] + + // Check if selected model is missing from the lists + const allModelIds = [...preferredModelIds, ...restModelIds] + const isMissingSelectedModel = selectedModelId && !allModelIds.includes(selectedModelId) + + // Add "Recommended models" section if there are preferred models + if (hasPreferred && preferredModelIds.length > 0) { + result.push({ + value: "__label_recommended__", + label: t("settings:modelPicker.recommendedModels"), + type: DropdownOptionType.LABEL, + }) + + // Add the missing selected model at the top if it was a preferred model + // (unlikely, but handle the edge case) + + preferredModelIds.forEach((modelId) => { + result.push({ + value: modelId, + label: providerModels[modelId]?.displayName ?? prettyModelName(modelId), + type: DropdownOptionType.ITEM, + }) + }) + } + + // Add "All models" section + if (restModelIds.length > 0) { + result.push({ + value: "__label_all__", + label: t("settings:modelPicker.allModels"), + type: DropdownOptionType.LABEL, + }) + + // Add missing selected model at the top of "All models" if not in any list + if (isMissingSelectedModel) { + result.push({ + value: selectedModelId, + label: providerModels[selectedModelId]?.displayName ?? prettyModelName(selectedModelId), + type: DropdownOptionType.ITEM, + }) + } + + restModelIds.forEach((modelId) => { + result.push({ + value: modelId, + label: providerModels[modelId]?.displayName ?? prettyModelName(modelId), + type: DropdownOptionType.ITEM, + }) + }) + } else if (isMissingSelectedModel) { + // If there are no rest models but we have a missing selected model, add it + result.push({ + value: selectedModelId, + label: providerModels[selectedModelId]?.displayName ?? prettyModelName(selectedModelId), + type: DropdownOptionType.ITEM, + }) + } + + return result + }, [preferredModelIds, restModelIds, hasPreferred, providerModels, selectedModelId, t]) const disabled = isLoading || isError || isAutocomplete @@ -67,7 +121,6 @@ export const ModelSelector = ({ return null } - // kilocode_change start: Display active model for virtual quota fallback if (provider === "virtual-quota-fallback" && virtualQuotaActiveModel) { return ( @@ -75,7 +128,6 @@ export const ModelSelector = ({ ) } - // kilocode_change end if (isError || isAutocomplete || options.length <= 0) { return {fallbackText} diff --git a/webview-ui/src/components/kilocode/chat/__tests__/ModelSelector.spec.tsx b/webview-ui/src/components/kilocode/chat/__tests__/ModelSelector.spec.tsx index e011713b8da..16612bbbac4 100644 --- a/webview-ui/src/components/kilocode/chat/__tests__/ModelSelector.spec.tsx +++ b/webview-ui/src/components/kilocode/chat/__tests__/ModelSelector.spec.tsx @@ -15,7 +15,11 @@ vi.mock("@/i18n/TranslationContext", () => ({ })) vi.mock("@/components/ui/hooks/kilocode/usePreferredModels", () => ({ - usePreferredModels: () => ["model-1", "model-2"], + useGroupedModelIds: () => ({ + preferredModelIds: [], + restModelIds: ["model-1", "model-2"], + hasPreferred: false, + }), })) // Create a mock function that can be controlled per test diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index 1f4fe4fc07e..d35b067d136 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, useRef, Fragment } from "react" // kilocode_change Fragment +import { useState, useCallback, useEffect, useRef, useMemo, Fragment } from "react" // kilocode_change Fragment, useMemo import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" import { Trans } from "react-i18next" import { ChevronsUpDown, Check, X, Info } from "lucide-react" @@ -7,7 +7,7 @@ import type { ProviderSettings, ModelInfo, OrganizationAllowList } from "@roo-co import { useAppTranslation } from "@src/i18n/TranslationContext" import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" -import { usePreferredModels } from "@/components/ui/hooks/kilocode/usePreferredModels" // kilocode_change +import { useGroupedModelIds } from "@/components/ui/hooks/kilocode/usePreferredModels" // kilocode_change: use grouped hook // import { filterModels } from "./utils/organizationFilters" // kilocode_change: not doing this import { cn } from "@src/lib/utils" import { @@ -21,7 +21,6 @@ import { PopoverContent, PopoverTrigger, Button, - SelectSeparator, // kilocode_change } from "@src/components/ui" import { useEscapeKey } from "@src/hooks/useEscapeKey" @@ -88,8 +87,9 @@ export const ModelPicker = ({ const selectTimeoutRef = useRef(null) const closeTimeoutRef = useRef(null) - // kilocode_change start - const modelIds = usePreferredModels(models) + // kilocode_change start: Use grouped model IDs for section headers + const { preferredModelIds, restModelIds, hasPreferred } = useGroupedModelIds(models) + const allModelIds = useMemo(() => [...preferredModelIds, ...restModelIds], [preferredModelIds, restModelIds]) const [isPricingExpanded, setIsPricingExpanded] = useState(false) // kilocode_change end @@ -139,12 +139,12 @@ export const ModelPicker = ({ useEffect(() => { if (!selectedModelId && !isInitialized.current) { - const initialValue = modelIds.includes(selectedModelId) ? selectedModelId : defaultModelId + const initialValue = allModelIds.includes(selectedModelId) ? selectedModelId : defaultModelId setApiConfigurationField(modelIdKey, initialValue, false) // false = automatic initialization } isInitialized.current = true - }, [modelIds, setApiConfigurationField, modelIdKey, selectedModelId, defaultModelId]) + }, [allModelIds, setApiConfigurationField, modelIdKey, selectedModelId, defaultModelId]) // kilocode_change: allModelIds replaces modelIds // Cleanup timeouts on unmount to prevent test flakiness useEffect(() => { @@ -205,38 +205,53 @@ export const ModelPicker = ({ )} - - {/* kilocode_change start */} - {modelIds.map((model, i) => { - const isPreferred = Number.isInteger(models?.[model]?.preferredIndex) - const previousModelWasPreferred = Number.isInteger( - models?.[modelIds[i - 1]]?.preferredIndex, - ) - return ( - - {!isPreferred && previousModelWasPreferred ? : null} - - - {model} - - - - - ) - })} - {/* kilocode_change end */} - + {/* kilocode_change start: Section headers for recommended and all models */} + {hasPreferred && preferredModelIds.length > 0 && ( + + {preferredModelIds.map((model) => ( + + + {model} + + + + ))} + + )} + {restModelIds.length > 0 && ( + + {restModelIds.map((model) => ( + + + {model} + + + + ))} + + )} + {/* kilocode_change end */} - {searchValue && !modelIds.includes(searchValue) && ( + {searchValue && !allModelIds.includes(searchValue) && (
{t("settings:modelPicker.useCustomModel", { modelId: searchValue })} diff --git a/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx b/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx index 2262adb8728..7ee9dc87703 100644 --- a/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx +++ b/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx @@ -254,6 +254,51 @@ describe("SelectDropdown", () => { expect(content).toBeInTheDocument() }) + // kilocode_change start: Tests for LABEL type (section headers) + it("renders label options as section headers", () => { + const optionsWithLabel = [ + { value: "__label_recommended__", label: "Recommended models", type: DropdownOptionType.LABEL }, + { value: "model1", label: "Model 1" }, + { value: "__label_all__", label: "All models", type: DropdownOptionType.LABEL }, + { value: "model2", label: "Model 2" }, + ] + + render() + + // Click the trigger to open the dropdown + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + // Now we can check for dropdown content + const content = screen.getByTestId("dropdown-content") + expect(content).toBeInTheDocument() + + // For this test, we'll just verify the content is rendered + // In a real scenario, we'd need to update the mock to properly handle label rendering + expect(content).toBeInTheDocument() + }) + + it("LABEL options are non-selectable", () => { + const optionsWithLabel = [ + { value: "__label_section__", label: "Section Header", type: DropdownOptionType.LABEL }, + { value: "option1", label: "Option 1" }, + ] + + render() + + // Click the trigger to open the dropdown + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + // The content should be rendered + const content = screen.getByTestId("dropdown-content") + expect(content).toBeInTheDocument() + + // LABEL items should not trigger onChange when interacted with + // The actual behavior is tested via integration since our mock doesn't fully simulate this + }) + // kilocode_change end + it("calls onChange for regular menu items", () => { render() diff --git a/webview-ui/src/components/ui/hooks/kilocode/__tests__/usePreferredModels.spec.ts b/webview-ui/src/components/ui/hooks/kilocode/__tests__/usePreferredModels.spec.ts new file mode 100644 index 00000000000..7a7373f6aa2 --- /dev/null +++ b/webview-ui/src/components/ui/hooks/kilocode/__tests__/usePreferredModels.spec.ts @@ -0,0 +1,132 @@ +// kilocode_change - new file +// npx vitest run src/components/ui/hooks/kilocode/__tests__/usePreferredModels.spec.ts + +import { renderHook } from "@testing-library/react" +import { useGroupedModelIds, getGroupedModelIds } from "../usePreferredModels" +import type { ModelInfo } from "@roo-code/types" + +// Helper to create minimal ModelInfo objects for testing +const createModelInfo = (overrides: Partial = {}): ModelInfo => ({ + contextWindow: 100000, + supportsPromptCache: false, + maxTokens: 4096, + ...overrides, +}) + +describe("getGroupedModelIds", () => { + it("returns empty arrays when models is null", () => { + const result = getGroupedModelIds(null) + + expect(result).toEqual({ + preferredModelIds: [], + restModelIds: [], + hasPreferred: false, + }) + }) + + it("returns empty arrays when models is empty", () => { + const result = getGroupedModelIds({}) + + expect(result).toEqual({ + preferredModelIds: [], + restModelIds: [], + hasPreferred: false, + }) + }) + + it("separates preferred models from rest models", () => { + const models: Record = { + "model-a": createModelInfo({ preferredIndex: 1 }), + "model-b": createModelInfo(), + "model-c": createModelInfo({ preferredIndex: 0 }), + "model-d": createModelInfo(), + } + + const result = getGroupedModelIds(models) + + expect(result.preferredModelIds).toEqual(["model-c", "model-a"]) + expect(result.restModelIds).toEqual(["model-b", "model-d"]) + expect(result.hasPreferred).toBe(true) + }) + + it("sorts preferred models by preferredIndex", () => { + const models: Record = { + "model-z": createModelInfo({ preferredIndex: 2 }), + "model-a": createModelInfo({ preferredIndex: 0 }), + "model-m": createModelInfo({ preferredIndex: 1 }), + } + + const result = getGroupedModelIds(models) + + expect(result.preferredModelIds).toEqual(["model-a", "model-m", "model-z"]) + expect(result.hasPreferred).toBe(true) + }) + + it("sorts rest models alphabetically", () => { + const models: Record = { + "zebra-model": createModelInfo(), + "alpha-model": createModelInfo(), + "beta-model": createModelInfo(), + } + + const result = getGroupedModelIds(models) + + expect(result.restModelIds).toEqual(["alpha-model", "beta-model", "zebra-model"]) + expect(result.hasPreferred).toBe(false) + }) + + it("handles case where all models are preferred", () => { + const models: Record = { + "model-a": createModelInfo({ preferredIndex: 0 }), + "model-b": createModelInfo({ preferredIndex: 1 }), + } + + const result = getGroupedModelIds(models) + + expect(result.preferredModelIds).toEqual(["model-a", "model-b"]) + expect(result.restModelIds).toEqual([]) + expect(result.hasPreferred).toBe(true) + }) + + it("handles case where no models are preferred", () => { + const models: Record = { + "model-a": createModelInfo(), + "model-b": createModelInfo(), + } + + const result = getGroupedModelIds(models) + + expect(result.preferredModelIds).toEqual([]) + expect(result.restModelIds).toEqual(["model-a", "model-b"]) + expect(result.hasPreferred).toBe(false) + }) +}) + +describe("useGroupedModelIds", () => { + it("returns grouped model IDs with hasPreferred flag", () => { + const models: Record = { + "pref-model": createModelInfo({ preferredIndex: 0 }), + "rest-model": createModelInfo(), + } + + const { result } = renderHook(() => useGroupedModelIds(models)) + + expect(result.current.preferredModelIds).toEqual(["pref-model"]) + expect(result.current.restModelIds).toEqual(["rest-model"]) + expect(result.current.hasPreferred).toBe(true) + }) + + it("memoizes result when models don't change", () => { + const models: Record = { + "model-a": createModelInfo(), + } + + const { result, rerender } = renderHook(() => useGroupedModelIds(models)) + const firstResult = result.current + + rerender() + const secondResult = result.current + + expect(firstResult).toBe(secondResult) + }) +}) diff --git a/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts b/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts index 847625aff7e..29d74bc0e45 100644 --- a/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts +++ b/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts @@ -1,33 +1,58 @@ +// kilocode_change - new file import { useMemo } from "react" import type { ModelInfo } from "@roo-code/types" -export const usePreferredModels = (models: Record | null) => { - return useMemo(() => { - if (!models) return [] - - const preferredModelIds = [] - const restModelIds = [] - // first add the preferred models - for (const [key, model] of Object.entries(models)) { - if (Number.isInteger(model.preferredIndex)) { - preferredModelIds.push(key) - } +/** + * Result containing preferred and rest model IDs, plus a flag indicating if there are preferred models + */ +export interface GroupedModelIds { + preferredModelIds: string[] + restModelIds: string[] + hasPreferred: boolean +} + +/** + * Extracts and groups model IDs into preferred and rest categories + */ +export const getGroupedModelIds = (models: Record | null): GroupedModelIds => { + if (!models) { + return { preferredModelIds: [], restModelIds: [], hasPreferred: false } + } + + const preferredModelIds: string[] = [] + const restModelIds: string[] = [] + + // First add the preferred models + for (const [key, model] of Object.entries(models)) { + if (Number.isInteger(model.preferredIndex)) { + preferredModelIds.push(key) } + } - preferredModelIds.sort((a, b) => { - const modelA = models[a] - const modelB = models[b] - return (modelA.preferredIndex ?? 0) - (modelB.preferredIndex ?? 0) - }) - - // then add the rest - for (const [key] of Object.entries(models)) { - if (!preferredModelIds.includes(key)) { - restModelIds.push(key) - } + preferredModelIds.sort((a, b) => { + const modelA = models[a] + const modelB = models[b] + return (modelA.preferredIndex ?? 0) - (modelB.preferredIndex ?? 0) + }) + + // Then add the rest + for (const [key] of Object.entries(models)) { + if (!preferredModelIds.includes(key)) { + restModelIds.push(key) } - restModelIds.sort((a, b) => a.localeCompare(b)) + } + restModelIds.sort((a, b) => a.localeCompare(b)) + + return { + preferredModelIds, + restModelIds, + hasPreferred: preferredModelIds.length > 0, + } +} - return [...preferredModelIds, ...restModelIds] - }, [models]) +/** + * Hook to get grouped model IDs with section metadata for sectioned dropdowns + */ +export const useGroupedModelIds = (models: Record | null): GroupedModelIds => { + return useMemo(() => getGroupedModelIds(models), [models]) } diff --git a/webview-ui/src/components/ui/select-dropdown.tsx b/webview-ui/src/components/ui/select-dropdown.tsx index 18b544f2d51..f695939ad6e 100644 --- a/webview-ui/src/components/ui/select-dropdown.tsx +++ b/webview-ui/src/components/ui/select-dropdown.tsx @@ -15,6 +15,7 @@ export enum DropdownOptionType { SEPARATOR = "separator", SHORTCUT = "shortcut", ACTION = "action", + LABEL = "label", // kilocode_change: Section header for grouped options } export interface DropdownOption { @@ -114,7 +115,9 @@ export const SelectDropdown = React.memo( return options .filter( (option) => - option.type !== DropdownOptionType.SEPARATOR && option.type !== DropdownOptionType.SHORTCUT, + option.type !== DropdownOptionType.SEPARATOR && + option.type !== DropdownOptionType.SHORTCUT && + option.type !== DropdownOptionType.LABEL, // kilocode_change: exclude LABEL from search ) .map((option) => ({ original: option, @@ -137,9 +140,13 @@ export const SelectDropdown = React.memo( // Get fuzzy matching items - only perform search if we have a search value const matchingItems = fzfInstance.find(searchValue).map((result) => result.item.original) - // Always include separators and shortcuts + // Always include separators, shortcuts, and labels return options.filter((option) => { - if (option.type === DropdownOptionType.SEPARATOR || option.type === DropdownOptionType.SHORTCUT) { + if ( + option.type === DropdownOptionType.SEPARATOR || + option.type === DropdownOptionType.SHORTCUT || + option.type === DropdownOptionType.LABEL // kilocode_change: include LABEL in filtered results + ) { return true } @@ -148,31 +155,61 @@ export const SelectDropdown = React.memo( }) }, [options, searchValue, fzfInstance, disableSearch]) - // Group options by type and handle separators + // Group options by type and handle separators and labels + // kilocode_change start: improved handling for section labels const groupedOptions = React.useMemo(() => { const result: DropdownOption[] = [] - let lastWasSeparator = false + let lastWasSeparatorOrLabel = false filteredOptions.forEach((option) => { if (option.type === DropdownOptionType.SEPARATOR) { // Only add separator if we have items before and after it - if (result.length > 0 && !lastWasSeparator) { + if (result.length > 0 && !lastWasSeparatorOrLabel) { result.push(option) - lastWasSeparator = true + lastWasSeparatorOrLabel = true } + } else if (option.type === DropdownOptionType.LABEL) { + // Track label position - we'll only keep it if it has items after it + result.push(option) + lastWasSeparatorOrLabel = true } else { result.push(option) - lastWasSeparator = false + lastWasSeparatorOrLabel = false } }) - // Remove trailing separator if present - if (result.length > 0 && result[result.length - 1].type === DropdownOptionType.SEPARATOR) { + // Remove trailing separator or label if present + while ( + result.length > 0 && + (result[result.length - 1].type === DropdownOptionType.SEPARATOR || + result[result.length - 1].type === DropdownOptionType.LABEL) + ) { result.pop() } - return result + // Also remove any labels that ended up with no items after them + // (can happen when filtering removes all items in a section) + const finalResult: DropdownOption[] = [] + for (let i = 0; i < result.length; i++) { + const option = result[i] + if (option.type === DropdownOptionType.LABEL) { + // Check if next item is also a label or separator (meaning this label has no items) + const nextItem = result[i + 1] + if ( + nextItem && + nextItem.type !== DropdownOptionType.LABEL && + nextItem.type !== DropdownOptionType.SEPARATOR + ) { + finalResult.push(option) + } + } else { + finalResult.push(option) + } + } + + return finalResult }, [filteredOptions]) + // kilocode_change end const handleSelect = React.useCallback( (optionValue: string) => { @@ -278,13 +315,30 @@ export const SelectDropdown = React.memo( ) } + { + /* kilocode_change start: render LABEL type as section header */ + } + if (option.type === DropdownOptionType.LABEL) { + return ( +
+ {option.label} +
+ ) + } + { + /* kilocode_change end */ + } + if ( option.type === DropdownOptionType.SHORTCUT || (option.disabled && shortcutText && option.label.includes(shortcutText)) ) { return (
{option.label}
diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 26e31178007..86922fa1b08 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -1058,7 +1058,9 @@ "searchPlaceholder": "Search", "noMatchFound": "No match found", "useCustomModel": "Use custom: {{modelId}}", - "simplifiedExplanation": "You can adjust detailed model settings later." + "simplifiedExplanation": "You can adjust detailed model settings later.", + "recommendedModels": "Recommended models", + "allModels": "All models" }, "footer": { "feedback": "If you have any questions or feedback, feel free to open an issue at github.com/Kilo-Org/kilocode or join reddit.com/r/kilocode or kilo.ai/discord.", From d55c093797c4a816a86ee5ee000f32a98f28199b Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Wed, 7 Jan 2026 11:03:20 -0500 Subject: [PATCH 02/10] Clean ups and comment fixes (hopefully) --- .changeset/model-picker-sections.md | 5 + .../kilocode/chat/ModelSelector.tsx | 5 +- .../chat/__tests__/ModelSelector.spec.tsx | 119 ++++++++++++++++-- .../src/components/settings/ModelPicker.tsx | 18 +-- .../ui/__tests__/select-dropdown.spec.tsx | 10 +- .../ui/hooks/kilocode/usePreferredModels.ts | 1 - webview-ui/src/i18n/locales/ar/settings.json | 4 +- webview-ui/src/i18n/locales/de/settings.json | 4 +- 8 files changed, 140 insertions(+), 26 deletions(-) create mode 100644 .changeset/model-picker-sections.md diff --git a/.changeset/model-picker-sections.md b/.changeset/model-picker-sections.md new file mode 100644 index 00000000000..7b0ffaabad2 --- /dev/null +++ b/.changeset/model-picker-sections.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Add section headers to model selection dropdowns for "Recommended models" and "All models" diff --git a/webview-ui/src/components/kilocode/chat/ModelSelector.tsx b/webview-ui/src/components/kilocode/chat/ModelSelector.tsx index 37f6fd1fd9a..abc9a9e3072 100644 --- a/webview-ui/src/components/kilocode/chat/ModelSelector.tsx +++ b/webview-ui/src/components/kilocode/chat/ModelSelector.tsx @@ -48,9 +48,6 @@ export const ModelSelector = ({ type: DropdownOptionType.LABEL, }) - // Add the missing selected model at the top if it was a preferred model - // (unlikely, but handle the edge case) - preferredModelIds.forEach((modelId) => { result.push({ value: modelId, @@ -121,6 +118,7 @@ export const ModelSelector = ({ return null } + // kilocode_change start: Display active model for virtual quota fallback if (provider === "virtual-quota-fallback" && virtualQuotaActiveModel) { return ( @@ -128,6 +126,7 @@ export const ModelSelector = ({ ) } + // kilocode_change end if (isError || isAutocomplete || options.length <= 0) { return {fallbackText} diff --git a/webview-ui/src/components/kilocode/chat/__tests__/ModelSelector.spec.tsx b/webview-ui/src/components/kilocode/chat/__tests__/ModelSelector.spec.tsx index 16612bbbac4..1e3953bf4ea 100644 --- a/webview-ui/src/components/kilocode/chat/__tests__/ModelSelector.spec.tsx +++ b/webview-ui/src/components/kilocode/chat/__tests__/ModelSelector.spec.tsx @@ -14,12 +14,11 @@ vi.mock("@/i18n/TranslationContext", () => ({ }), })) +// Create a mock function that can be controlled per test +const mockUseGroupedModelIds = vi.fn() + vi.mock("@/components/ui/hooks/kilocode/usePreferredModels", () => ({ - useGroupedModelIds: () => ({ - preferredModelIds: [], - restModelIds: ["model-1", "model-2"], - hasPreferred: false, - }), + useGroupedModelIds: () => mockUseGroupedModelIds(), })) // Create a mock function that can be controlled per test @@ -41,9 +40,18 @@ describe("ModelSelector", () => { } beforeEach(() => { - // Reset mock before each test + // Reset mocks before each test mockUseProviderModels.mockReset() - // Default mock implementation + mockUseGroupedModelIds.mockReset() + + // Default mock implementation for useGroupedModelIds (no preferred models) + mockUseGroupedModelIds.mockReturnValue({ + preferredModelIds: [], + restModelIds: ["model-1", "model-2"], + hasPreferred: false, + }) + + // Default mock implementation for useProviderModels mockUseProviderModels.mockReturnValue({ provider: "openai", providerModels: { @@ -192,4 +200,101 @@ describe("ModelSelector", () => { const dropdownTrigger = screen.queryByTestId("dropdown-trigger") expect(dropdownTrigger).not.toBeInTheDocument() }) + + describe("preferred models sections", () => { + test("builds options with section headers when preferred models exist", () => { + // Setup mock to return preferred models + mockUseGroupedModelIds.mockReturnValue({ + preferredModelIds: ["preferred-1", "preferred-2"], + restModelIds: ["model-1", "model-2"], + hasPreferred: true, + }) + + mockUseProviderModels.mockReturnValue({ + provider: "openai", + providerModels: { + "preferred-1": { displayName: "Preferred Model 1", preferredIndex: 0 }, + "preferred-2": { displayName: "Preferred Model 2", preferredIndex: 1 }, + "model-1": { displayName: "Model 1" }, + "model-2": { displayName: "Model 2" }, + }, + providerDefaultModel: "model-1", + isLoading: false, + isError: false, + }) + + render( + , + ) + + // Should render the dropdown + const dropdownTrigger = screen.getByTestId("dropdown-trigger") + expect(dropdownTrigger).toBeInTheDocument() + }) + + test("does not add section headers when no preferred models exist", () => { + // Setup mock with no preferred models + mockUseGroupedModelIds.mockReturnValue({ + preferredModelIds: [], + restModelIds: ["model-1", "model-2"], + hasPreferred: false, + }) + + render( + , + ) + + // Should render the dropdown + const dropdownTrigger = screen.getByTestId("dropdown-trigger") + expect(dropdownTrigger).toBeInTheDocument() + }) + + test("handles only preferred models without rest models", () => { + // Setup mock with only preferred models (edge case) + mockUseGroupedModelIds.mockReturnValue({ + preferredModelIds: ["preferred-1"], + restModelIds: [], + hasPreferred: true, + }) + + mockUseProviderModels.mockReturnValue({ + provider: "openai", + providerModels: { + "preferred-1": { displayName: "Preferred Model 1", preferredIndex: 0 }, + }, + providerDefaultModel: "preferred-1", + isLoading: false, + isError: false, + }) + + render( + , + ) + + // Should render the dropdown with only preferred models section + const dropdownTrigger = screen.getByTestId("dropdown-trigger") + expect(dropdownTrigger).toBeInTheDocument() + }) + }) }) diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index d35b067d136..86a88d8e429 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -251,13 +251,17 @@ export const ModelPicker = ({ )} {/* kilocode_change end */} - {searchValue && !allModelIds.includes(searchValue) && ( -
- - {t("settings:modelPicker.useCustomModel", { modelId: searchValue })} - -
- )} + {searchValue && + !allModelIds.includes(searchValue) && ( // kilocode_change: allModelIds replaces modelIds +
+ + {t("settings:modelPicker.useCustomModel", { modelId: searchValue })} + +
+ )} diff --git a/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx b/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx index 7ee9dc87703..ab892167b2c 100644 --- a/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx +++ b/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx @@ -272,10 +272,6 @@ describe("SelectDropdown", () => { // Now we can check for dropdown content const content = screen.getByTestId("dropdown-content") expect(content).toBeInTheDocument() - - // For this test, we'll just verify the content is rendered - // In a real scenario, we'd need to update the mock to properly handle label rendering - expect(content).toBeInTheDocument() }) it("LABEL options are non-selectable", () => { @@ -294,8 +290,10 @@ describe("SelectDropdown", () => { const content = screen.getByTestId("dropdown-content") expect(content).toBeInTheDocument() - // LABEL items should not trigger onChange when interacted with - // The actual behavior is tested via integration since our mock doesn't fully simulate this + // Note: The actual component renders LABEL items as static divs without onClick handlers + // (with data-testid="dropdown-label"), making them inherently non-selectable. + // This test uses mocks, so we just verify the component renders correctly. + // The non-selectability behavior is ensured by the component's implementation. }) // kilocode_change end diff --git a/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts b/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts index 29d74bc0e45..65d9b9637c5 100644 --- a/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts +++ b/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts @@ -1,4 +1,3 @@ -// kilocode_change - new file import { useMemo } from "react" import type { ModelInfo } from "@roo-code/types" diff --git a/webview-ui/src/i18n/locales/ar/settings.json b/webview-ui/src/i18n/locales/ar/settings.json index 4bb57148c69..5438e6de237 100644 --- a/webview-ui/src/i18n/locales/ar/settings.json +++ b/webview-ui/src/i18n/locales/ar/settings.json @@ -1134,7 +1134,9 @@ "searchPlaceholder": "بحث", "noMatchFound": "ما فيه تطابق", "useCustomModel": "استخدام مخصص: {{modelId}}", - "simplifiedExplanation": "يمكنك ضبط إعدادات النموذج التفصيلية لاحقًا." + "simplifiedExplanation": "يمكنك ضبط إعدادات النموذج التفصيلية لاحقًا.", + "recommendedModels": "Recommended models", + "allModels": "All models" }, "footer": { "feedback": "عندك سؤال أو ملاحظة؟ افتح تذكرة في github.com/Kilo-Org/kilocode أو انضم لـ r/kilocode أو kilo.ai/discord.", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 9cce9de263a..aa9ca46dd50 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1021,7 +1021,9 @@ "searchPlaceholder": "Suchen", "noMatchFound": "Keine Übereinstimmung gefunden", "useCustomModel": "Benutzerdefiniert verwenden: {{modelId}}", - "simplifiedExplanation": "Du kannst detaillierte Modelleinstellungen später anpassen." + "simplifiedExplanation": "Du kannst detaillierte Modelleinstellungen später anpassen.", + "recommendedModels": "Recommended models", + "allModels": "All models" }, "footer": { "feedback": "Wenn du Fragen oder Feedback hast, kannst du gerne ein Issue auf github.com/Kilo-Org/kilocode öffnen oder reddit.com/r/kilocode oder kilo.ai/discord beitreten", From 1f5e4d59567f3213a3eaa0abc9c9e3fff9e84b71 Mon Sep 17 00:00:00 2001 From: Joshua Lambert <25085430+lambertjosh@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:47:23 -0500 Subject: [PATCH 03/10] Update webview-ui/src/components/settings/ModelPicker.tsx --- webview-ui/src/components/settings/ModelPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index 86a88d8e429..01fb5815090 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -139,7 +139,7 @@ export const ModelPicker = ({ useEffect(() => { if (!selectedModelId && !isInitialized.current) { - const initialValue = allModelIds.includes(selectedModelId) ? selectedModelId : defaultModelId + const initialValue = allModelIds.includes(selectedModelId) ? selectedModelId : defaultModelId // kilocode_change setApiConfigurationField(modelIdKey, initialValue, false) // false = automatic initialization } From d511d6308cf0c9b8a945737cbe4a11db70f69851 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Wed, 7 Jan 2026 14:27:53 -0500 Subject: [PATCH 04/10] Re-use existing variable name --- .../src/components/settings/ModelPicker.tsx | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index 01fb5815090..5eecd933a82 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -7,7 +7,7 @@ import type { ProviderSettings, ModelInfo, OrganizationAllowList } from "@roo-co import { useAppTranslation } from "@src/i18n/TranslationContext" import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" -import { useGroupedModelIds } from "@/components/ui/hooks/kilocode/usePreferredModels" // kilocode_change: use grouped hook +import { useGroupedModelIds } from "@/components/ui/hooks/kilocode/usePreferredModels" // kilocode_change // import { filterModels } from "./utils/organizationFilters" // kilocode_change: not doing this import { cn } from "@src/lib/utils" import { @@ -89,7 +89,7 @@ export const ModelPicker = ({ // kilocode_change start: Use grouped model IDs for section headers const { preferredModelIds, restModelIds, hasPreferred } = useGroupedModelIds(models) - const allModelIds = useMemo(() => [...preferredModelIds, ...restModelIds], [preferredModelIds, restModelIds]) + const modelIds = useMemo(() => [...preferredModelIds, ...restModelIds], [preferredModelIds, restModelIds]) const [isPricingExpanded, setIsPricingExpanded] = useState(false) // kilocode_change end @@ -139,12 +139,12 @@ export const ModelPicker = ({ useEffect(() => { if (!selectedModelId && !isInitialized.current) { - const initialValue = allModelIds.includes(selectedModelId) ? selectedModelId : defaultModelId // kilocode_change + const initialValue = modelIds.includes(selectedModelId) ? selectedModelId : defaultModelId setApiConfigurationField(modelIdKey, initialValue, false) // false = automatic initialization } isInitialized.current = true - }, [allModelIds, setApiConfigurationField, modelIdKey, selectedModelId, defaultModelId]) // kilocode_change: allModelIds replaces modelIds + }, [modelIds, setApiConfigurationField, modelIdKey, selectedModelId, defaultModelId]) // Cleanup timeouts on unmount to prevent test flakiness useEffect(() => { @@ -251,17 +251,13 @@ export const ModelPicker = ({ )} {/* kilocode_change end */} - {searchValue && - !allModelIds.includes(searchValue) && ( // kilocode_change: allModelIds replaces modelIds -
- - {t("settings:modelPicker.useCustomModel", { modelId: searchValue })} - -
- )} + {searchValue && !modelIds.includes(searchValue) && ( +
+ + {t("settings:modelPicker.useCustomModel", { modelId: searchValue })} + +
+ )} From a869c2c00b6f450b7fdba0be4f49c80f78cc0c83 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Wed, 7 Jan 2026 14:50:46 -0500 Subject: [PATCH 05/10] Fix extra braces --- webview-ui/src/components/ui/select-dropdown.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/webview-ui/src/components/ui/select-dropdown.tsx b/webview-ui/src/components/ui/select-dropdown.tsx index f695939ad6e..09b8a622e25 100644 --- a/webview-ui/src/components/ui/select-dropdown.tsx +++ b/webview-ui/src/components/ui/select-dropdown.tsx @@ -315,9 +315,7 @@ export const SelectDropdown = React.memo( ) } - { - /* kilocode_change start: render LABEL type as section header */ - } + // kilocode_change start: render LABEL type as section header if (option.type === DropdownOptionType.LABEL) { return (
) } - { - /* kilocode_change end */ - } + // kilocode_change end if ( option.type === DropdownOptionType.SHORTCUT || From d80a4e5ac18d613eef4905f265257ba8a909aa5d Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Wed, 7 Jan 2026 15:26:24 -0500 Subject: [PATCH 06/10] Add translations --- webview-ui/src/i18n/locales/ar/settings.json | 4 ++-- webview-ui/src/i18n/locales/ca/settings.json | 4 +++- webview-ui/src/i18n/locales/cs/settings.json | 4 +++- webview-ui/src/i18n/locales/de/settings.json | 4 ++-- webview-ui/src/i18n/locales/es/settings.json | 4 +++- webview-ui/src/i18n/locales/fr/settings.json | 4 +++- webview-ui/src/i18n/locales/hi/settings.json | 4 +++- webview-ui/src/i18n/locales/id/settings.json | 4 +++- webview-ui/src/i18n/locales/it/settings.json | 4 +++- webview-ui/src/i18n/locales/ja/settings.json | 4 +++- webview-ui/src/i18n/locales/ko/settings.json | 4 +++- webview-ui/src/i18n/locales/nl/settings.json | 4 +++- webview-ui/src/i18n/locales/pl/settings.json | 4 +++- webview-ui/src/i18n/locales/pt-BR/settings.json | 4 +++- webview-ui/src/i18n/locales/ru/settings.json | 4 +++- webview-ui/src/i18n/locales/th/settings.json | 4 +++- webview-ui/src/i18n/locales/tr/settings.json | 4 +++- webview-ui/src/i18n/locales/uk/settings.json | 4 +++- webview-ui/src/i18n/locales/vi/settings.json | 4 +++- webview-ui/src/i18n/locales/zh-CN/settings.json | 4 +++- webview-ui/src/i18n/locales/zh-TW/settings.json | 4 +++- 21 files changed, 61 insertions(+), 23 deletions(-) diff --git a/webview-ui/src/i18n/locales/ar/settings.json b/webview-ui/src/i18n/locales/ar/settings.json index 5438e6de237..878ab64c632 100644 --- a/webview-ui/src/i18n/locales/ar/settings.json +++ b/webview-ui/src/i18n/locales/ar/settings.json @@ -1135,8 +1135,8 @@ "noMatchFound": "ما فيه تطابق", "useCustomModel": "استخدام مخصص: {{modelId}}", "simplifiedExplanation": "يمكنك ضبط إعدادات النموذج التفصيلية لاحقًا.", - "recommendedModels": "Recommended models", - "allModels": "All models" + "recommendedModels": "النماذج الموصى بها", + "allModels": "جميع النماذج" }, "footer": { "feedback": "عندك سؤال أو ملاحظة؟ افتح تذكرة في github.com/Kilo-Org/kilocode أو انضم لـ r/kilocode أو kilo.ai/discord.", diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index c7a1f577afe..9e3484e4e76 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1025,7 +1025,9 @@ "searchPlaceholder": "Cerca", "noMatchFound": "No s'ha trobat cap coincidència", "useCustomModel": "Utilitzar personalitzat: {{modelId}}", - "simplifiedExplanation": "Pots ajustar la configuració detallada del model més tard." + "simplifiedExplanation": "Pots ajustar la configuració detallada del model més tard.", + "recommendedModels": "Models recomanats", + "allModels": "Tots els models" }, "footer": { "feedback": "Si teniu qualsevol pregunta o comentari, no dubteu a obrir un issue a github.com/Kilo-Org/kilocode o unir-vos a reddit.com/r/kilocode o kilo.ai/discord", diff --git a/webview-ui/src/i18n/locales/cs/settings.json b/webview-ui/src/i18n/locales/cs/settings.json index b30ef443623..681690e1a15 100644 --- a/webview-ui/src/i18n/locales/cs/settings.json +++ b/webview-ui/src/i18n/locales/cs/settings.json @@ -1110,7 +1110,9 @@ "searchPlaceholder": "Hledat", "noMatchFound": "Nebyla nalezena žádná shoda", "useCustomModel": "Použít vlastní: {{modelId}}", - "simplifiedExplanation": "Podrobná nastavení modelu můžete upravit později." + "simplifiedExplanation": "Podrobná nastavení modelu můžete upravit později.", + "recommendedModels": "Doporučené modely", + "allModels": "Všechny modely" }, "footer": { "feedback": "Pokud máte nějaké dotazy nebo zpětnou vazbu, neváhejte otevřít problém na github.com/Kilo-Org/kilocode nebo se připojte k reddit.com/r/kilocode nebo kilo.ai/discord.", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index aa9ca46dd50..43a3a59ca9f 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1022,8 +1022,8 @@ "noMatchFound": "Keine Übereinstimmung gefunden", "useCustomModel": "Benutzerdefiniert verwenden: {{modelId}}", "simplifiedExplanation": "Du kannst detaillierte Modelleinstellungen später anpassen.", - "recommendedModels": "Recommended models", - "allModels": "All models" + "recommendedModels": "Empfohlene Modelle", + "allModels": "Alle Modelle" }, "footer": { "feedback": "Wenn du Fragen oder Feedback hast, kannst du gerne ein Issue auf github.com/Kilo-Org/kilocode öffnen oder reddit.com/r/kilocode oder kilo.ai/discord beitreten", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 96a2abd3c59..18d7766d3ef 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1025,7 +1025,9 @@ "searchPlaceholder": "Buscar", "noMatchFound": "No se encontraron coincidencias", "useCustomModel": "Usar personalizado: {{modelId}}", - "simplifiedExplanation": "Puedes ajustar la configuración detallada del modelo más tarde." + "simplifiedExplanation": "Puedes ajustar la configuración detallada del modelo más tarde.", + "recommendedModels": "Modelos recomendados", + "allModels": "Todos los modelos" }, "footer": { "feedback": "Si tiene alguna pregunta o comentario, no dude en abrir un issue en github.com/Kilo-Org/kilocode o unirse a reddit.com/r/kilocode o kilo.ai/discord", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index c713296abb9..9bafa973f6a 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1025,7 +1025,9 @@ "searchPlaceholder": "Rechercher", "noMatchFound": "Aucune correspondance trouvée", "useCustomModel": "Utiliser personnalisé : {{modelId}}", - "simplifiedExplanation": "Tu peux ajuster les paramètres détaillés du modèle ultérieurement." + "simplifiedExplanation": "Tu peux ajuster les paramètres détaillés du modèle ultérieurement.", + "recommendedModels": "Modèles recommandés", + "allModels": "Tous les modèles" }, "footer": { "feedback": "Si vous avez des questions ou des commentaires, n'hésitez pas à ouvrir un problème sur github.com/Kilo-Org/kilocode ou à rejoindre reddit.com/r/kilocode ou kilo.ai/discord", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 5f170c81e49..e89acea7084 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -1026,7 +1026,9 @@ "searchPlaceholder": "खोजें", "noMatchFound": "कोई मिलान नहीं मिला", "useCustomModel": "कस्टम उपयोग करें: {{modelId}}", - "simplifiedExplanation": "आप बाद में विस्तृत मॉडल सेटिंग्स समायोजित कर सकते हैं।" + "simplifiedExplanation": "आप बाद में विस्तृत मॉडल सेटिंग्स समायोजित कर सकते हैं।", + "recommendedModels": "अनुशंसित मॉडल", + "allModels": "सभी मॉडल" }, "footer": { "feedback": "यदि आपके कोई प्रश्न या प्रतिक्रिया है, तो github.com/Kilo-Org/kilocode पर एक मुद्दा खोलने या reddit.com/r/kilocode या kilo.ai/discord में शामिल होने में संकोच न करें", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 1f2969b257f..faa10d688f2 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1047,7 +1047,9 @@ "searchPlaceholder": "Cari", "noMatchFound": "Tidak ada yang cocok ditemukan", "useCustomModel": "Gunakan kustom: {{modelId}}", - "simplifiedExplanation": "Anda dapat menyesuaikan pengaturan model terperinci nanti." + "simplifiedExplanation": "Anda dapat menyesuaikan pengaturan model terperinci nanti.", + "recommendedModels": "Model yang direkomendasikan", + "allModels": "Semua model" }, "footer": { "feedback": "Jika kamu punya pertanyaan atau feedback, jangan ragu untuk membuka issue di github.com/Kilo-Org/kilocode atau bergabung reddit.com/r/kilocode atau kilo.ai/discord", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 70455bee4e5..f91935e7aab 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1027,7 +1027,9 @@ "searchPlaceholder": "Cerca", "noMatchFound": "Nessuna corrispondenza trovata", "useCustomModel": "Usa personalizzato: {{modelId}}", - "simplifiedExplanation": "Puoi modificare le impostazioni dettagliate del modello in seguito." + "simplifiedExplanation": "Puoi modificare le impostazioni dettagliate del modello in seguito.", + "recommendedModels": "Modelli consigliati", + "allModels": "Tutti i modelli" }, "footer": { "feedback": "Se hai domande o feedback, sentiti libero di aprire un issue su github.com/Kilo-Org/kilocode o unirti a reddit.com/r/kilocode o kilo.ai/discord", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index ccf1e2b664b..360d47cedb8 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -1027,7 +1027,9 @@ "searchPlaceholder": "検索", "noMatchFound": "一致するものが見つかりません", "useCustomModel": "カスタムを使用: {{modelId}}", - "simplifiedExplanation": "詳細なモデル設定は後で調整できます。" + "simplifiedExplanation": "詳細なモデル設定は後で調整できます。", + "recommendedModels": "おすすめのモデル", + "allModels": "すべてのモデル" }, "footer": { "feedback": "質問やフィードバックがある場合は、github.com/Kilo-Org/kilocodeで問題を開くか、reddit.com/r/kilocodekilo.ai/discordに参加してください", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 8f6079715ef..056852c59c0 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1026,7 +1026,9 @@ "searchPlaceholder": "검색", "noMatchFound": "일치하는 항목 없음", "useCustomModel": "사용자 정의 사용: {{modelId}}", - "simplifiedExplanation": "나중에 자세한 모델 설정을 조정할 수 있습니다." + "simplifiedExplanation": "나중에 자세한 모델 설정을 조정할 수 있습니다.", + "recommendedModels": "추천 모델", + "allModels": "모든 모델" }, "footer": { "feedback": "질문이나 피드백이 있으시면 github.com/Kilo-Org/kilocode에서 이슈를 열거나 reddit.com/r/kilocode 또는 kilo.ai/discord에 가입하세요", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 6e9e5d29768..0f20c587ced 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1026,7 +1026,9 @@ "searchPlaceholder": "Zoeken", "noMatchFound": "Geen overeenkomsten gevonden", "useCustomModel": "Aangepast gebruiken: {{modelId}}", - "simplifiedExplanation": "Je kunt later gedetailleerde modelinstellingen aanpassen." + "simplifiedExplanation": "Je kunt later gedetailleerde modelinstellingen aanpassen.", + "recommendedModels": "Aanbevolen modellen", + "allModels": "Alle modellen" }, "footer": { "feedback": "Heb je vragen of feedback? Open gerust een issue op github.com/Kilo-Org/kilocode of sluit je aan bij reddit.com/r/kilocode of kilo.ai/discord", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index df1b606903e..175526d483f 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1026,7 +1026,9 @@ "searchPlaceholder": "Wyszukaj", "noMatchFound": "Nie znaleziono dopasowań", "useCustomModel": "Użyj niestandardowy: {{modelId}}", - "simplifiedExplanation": "Można dostosować szczegółowe ustawienia modelu później." + "simplifiedExplanation": "Można dostosować szczegółowe ustawienia modelu później.", + "recommendedModels": "Polecane modele", + "allModels": "Wszystkie modele" }, "footer": { "feedback": "Jeśli masz jakiekolwiek pytania lub opinie, śmiało otwórz zgłoszenie na github.com/Kilo-Org/kilocode lub dołącz do reddit.com/r/kilocode lub kilo.ai/discord", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 05254deeebe..539640148a8 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1026,7 +1026,9 @@ "searchPlaceholder": "Pesquisar", "noMatchFound": "Nenhuma correspondência encontrada", "useCustomModel": "Usar personalizado: {{modelId}}", - "simplifiedExplanation": "Você pode ajustar as configurações detalhadas do modelo mais tarde." + "simplifiedExplanation": "Você pode ajustar as configurações detalhadas do modelo mais tarde.", + "recommendedModels": "Modelos recomendados", + "allModels": "Todos os modelos" }, "footer": { "feedback": "Se tiver alguma dúvida ou feedback, sinta-se à vontade para abrir um problema em github.com/Kilo-Org/kilocode ou juntar-se a reddit.com/r/kilocode ou kilo.ai/discord", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 80f8af0d31f..541dbb889aa 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1026,7 +1026,9 @@ "searchPlaceholder": "Поиск", "noMatchFound": "Совпадений не найдено", "useCustomModel": "Использовать пользовательскую: {{modelId}}", - "simplifiedExplanation": "Ты сможешь настроить подробные параметры модели позже." + "simplifiedExplanation": "Ты сможешь настроить подробные параметры модели позже.", + "recommendedModels": "Рекомендуемые модели", + "allModels": "Все модели" }, "footer": { "feedback": "Если у вас есть вопросы или предложения, откройте issue на github.com/Kilo-Org/kilocode или присоединяйтесь к reddit.com/r/kilocode или kilo.ai/discord", diff --git a/webview-ui/src/i18n/locales/th/settings.json b/webview-ui/src/i18n/locales/th/settings.json index 6920f3b78cf..8d57cf6a49e 100644 --- a/webview-ui/src/i18n/locales/th/settings.json +++ b/webview-ui/src/i18n/locales/th/settings.json @@ -1121,7 +1121,9 @@ "searchPlaceholder": "ค้นหา", "noMatchFound": "ไม่พบรายการที่ตรงกัน", "useCustomModel": "ใช้แบบกำหนดเอง: {{modelId}}", - "simplifiedExplanation": "คุณสามารถปรับการตั้งค่าโมเดลโดยละเอียดได้ภายหลัง" + "simplifiedExplanation": "คุณสามารถปรับการตั้งค่าโมเดลโดยละเอียดได้ภายหลัง", + "recommendedModels": "โมเดลที่แนะนำ", + "allModels": "โมเดลทั้งหมด" }, "footer": { "feedback": "หากคุณมีคำถามหรือข้อเสนอแนะ โปรดเปิด issue ที่ github.com/Kilo-Org/kilocode หรือเข้าร่วม reddit.com/r/kilocode หรือ kilo.ai/discord", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 03253251547..5fac19b4a80 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -1027,7 +1027,9 @@ "searchPlaceholder": "Ara", "noMatchFound": "Eşleşme bulunamadı", "useCustomModel": "Özel kullan: {{modelId}}", - "simplifiedExplanation": "Ayrıntılı model ayarlarını daha sonra ayarlayabilirsiniz." + "simplifiedExplanation": "Ayrıntılı model ayarlarını daha sonra ayarlayabilirsiniz.", + "recommendedModels": "Önerilen modeller", + "allModels": "Tüm modeller" }, "footer": { "feedback": "Herhangi bir sorunuz veya geri bildiriminiz varsa, github.com/Kilo-Org/kilocode adresinde bir konu açmaktan veya reddit.com/r/kilocode ya da kilo.ai/discord'a katılmaktan çekinmeyin", diff --git a/webview-ui/src/i18n/locales/uk/settings.json b/webview-ui/src/i18n/locales/uk/settings.json index a64c5be61ca..83e9a866f24 100644 --- a/webview-ui/src/i18n/locales/uk/settings.json +++ b/webview-ui/src/i18n/locales/uk/settings.json @@ -1135,7 +1135,9 @@ "searchPlaceholder": "Пошук", "noMatchFound": "Збігів не знайдено", "useCustomModel": "Використовувати власну: {{modelId}}", - "simplifiedExplanation": "Ви можете налаштувати детальні параметри моделі пізніше." + "simplifiedExplanation": "Ви можете налаштувати детальні параметри моделі пізніше.", + "recommendedModels": "Рекомендовані моделі", + "allModels": "Усі моделі" }, "footer": { "feedback": "Якщо у тебе є питання або відгуки, не соромся відкрити issue на github.com/Kilo-Org/kilocode або приєднатися до reddit.com/r/kilocode або kilo.ai/discord.", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index b7a24d909a8..24f762a714f 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -1026,7 +1026,9 @@ "searchPlaceholder": "Tìm kiếm", "noMatchFound": "Không tìm thấy kết quả", "useCustomModel": "Sử dụng tùy chỉnh: {{modelId}}", - "simplifiedExplanation": "Bạn có thể điều chỉnh cài đặt mô hình chi tiết sau." + "simplifiedExplanation": "Bạn có thể điều chỉnh cài đặt mô hình chi tiết sau.", + "recommendedModels": "Mô hình được đề xuất", + "allModels": "Tất cả mô hình" }, "footer": { "feedback": "Nếu bạn có bất kỳ câu hỏi hoặc phản hồi nào, vui lòng mở một vấn đề tại github.com/Kilo-Org/kilocode hoặc tham gia reddit.com/r/kilocode hoặc kilo.ai/discord", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index d4db3a3c159..919383096e9 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -1030,7 +1030,9 @@ "searchPlaceholder": "搜索", "noMatchFound": "未找到匹配项", "useCustomModel": "使用自定义:{{modelId}}", - "simplifiedExplanation": "你可以稍后调整详细的模型设置。" + "simplifiedExplanation": "你可以稍后调整详细的模型设置。", + "recommendedModels": "推荐模型", + "allModels": "全部模型" }, "footer": { "feedback": "如果您有任何问题或反馈,请随时在 github.com/Kilo-Org/kilocode 上提出问题或加入 reddit.com/r/kilocodekilo.ai/discord", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 3303f9bc025..25a74df1127 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -1027,7 +1027,9 @@ "searchPlaceholder": "搜尋", "noMatchFound": "找不到符合的項目", "useCustomModel": "使用自訂模型:{{modelId}}", - "simplifiedExplanation": "你可以稍後調整詳細的模型設定。" + "simplifiedExplanation": "你可以稍後調整詳細的模型設定。", + "recommendedModels": "推薦模型", + "allModels": "所有模型" }, "footer": { "feedback": "若您有任何問題或建議,歡迎至 github.com/Kilo-Org/kilocode 提出 issue,或加入 reddit.com/r/kilocodekilo.ai/discord 討論。", From 9619a576288d389057584678a2d4a95af9115022 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Wed, 7 Jan 2026 15:30:33 -0500 Subject: [PATCH 07/10] Simplify comments --- .../ui/hooks/kilocode/usePreferredModels.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts b/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts index 65d9b9637c5..92ee7c3b10a 100644 --- a/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts +++ b/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts @@ -1,18 +1,14 @@ import { useMemo } from "react" import type { ModelInfo } from "@roo-code/types" -/** - * Result containing preferred and rest model IDs, plus a flag indicating if there are preferred models - */ +// Result containing preferred and rest model IDs, plus a flag indicating if there are preferred models export interface GroupedModelIds { preferredModelIds: string[] restModelIds: string[] hasPreferred: boolean } -/** - * Extracts and groups model IDs into preferred and rest categories - */ +// Extracts and groups model IDs into preferred and rest categories export const getGroupedModelIds = (models: Record | null): GroupedModelIds => { if (!models) { return { preferredModelIds: [], restModelIds: [], hasPreferred: false } @@ -49,9 +45,7 @@ export const getGroupedModelIds = (models: Record | null): Gr } } -/** - * Hook to get grouped model IDs with section metadata for sectioned dropdowns - */ +// Hook to get grouped model IDs with section metadata for sectioned dropdowns export const useGroupedModelIds = (models: Record | null): GroupedModelIds => { return useMemo(() => getGroupedModelIds(models), [models]) } From 5e8812a250057c72f084eac03359534c6667cbfe Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Thu, 8 Jan 2026 16:22:35 -0500 Subject: [PATCH 08/10] Apply reviewer feedback --- .../kilocode/chat/ModelSelector.tsx | 6 +-- .../chat/__tests__/ModelSelector.spec.tsx | 4 -- .../src/components/settings/ModelPicker.tsx | 4 +- .../ui/__tests__/select-dropdown.spec.tsx | 43 ------------------- .../__tests__/usePreferredModels.spec.ts | 10 +---- .../ui/hooks/kilocode/usePreferredModels.ts | 6 +-- .../src/components/ui/select-dropdown.tsx | 2 +- 7 files changed, 9 insertions(+), 66 deletions(-) diff --git a/webview-ui/src/components/kilocode/chat/ModelSelector.tsx b/webview-ui/src/components/kilocode/chat/ModelSelector.tsx index abc9a9e3072..a22fb30538b 100644 --- a/webview-ui/src/components/kilocode/chat/ModelSelector.tsx +++ b/webview-ui/src/components/kilocode/chat/ModelSelector.tsx @@ -32,7 +32,7 @@ export const ModelSelector = ({ const modelIdKey = getModelIdKey({ provider }) const isAutocomplete = apiConfiguration.profileType === "autocomplete" - const { preferredModelIds, restModelIds, hasPreferred } = useGroupedModelIds(providerModels) + const { preferredModelIds, restModelIds } = useGroupedModelIds(providerModels) const options = useMemo(() => { const result: DropdownOption[] = [] @@ -41,7 +41,7 @@ export const ModelSelector = ({ const isMissingSelectedModel = selectedModelId && !allModelIds.includes(selectedModelId) // Add "Recommended models" section if there are preferred models - if (hasPreferred && preferredModelIds.length > 0) { + if (preferredModelIds.length > 0) { result.push({ value: "__label_recommended__", label: t("settings:modelPicker.recommendedModels"), @@ -91,7 +91,7 @@ export const ModelSelector = ({ } return result - }, [preferredModelIds, restModelIds, hasPreferred, providerModels, selectedModelId, t]) + }, [preferredModelIds, restModelIds, providerModels, selectedModelId, t]) const disabled = isLoading || isError || isAutocomplete diff --git a/webview-ui/src/components/kilocode/chat/__tests__/ModelSelector.spec.tsx b/webview-ui/src/components/kilocode/chat/__tests__/ModelSelector.spec.tsx index 1e3953bf4ea..daca799fb0d 100644 --- a/webview-ui/src/components/kilocode/chat/__tests__/ModelSelector.spec.tsx +++ b/webview-ui/src/components/kilocode/chat/__tests__/ModelSelector.spec.tsx @@ -48,7 +48,6 @@ describe("ModelSelector", () => { mockUseGroupedModelIds.mockReturnValue({ preferredModelIds: [], restModelIds: ["model-1", "model-2"], - hasPreferred: false, }) // Default mock implementation for useProviderModels @@ -207,7 +206,6 @@ describe("ModelSelector", () => { mockUseGroupedModelIds.mockReturnValue({ preferredModelIds: ["preferred-1", "preferred-2"], restModelIds: ["model-1", "model-2"], - hasPreferred: true, }) mockUseProviderModels.mockReturnValue({ @@ -244,7 +242,6 @@ describe("ModelSelector", () => { mockUseGroupedModelIds.mockReturnValue({ preferredModelIds: [], restModelIds: ["model-1", "model-2"], - hasPreferred: false, }) render( @@ -268,7 +265,6 @@ describe("ModelSelector", () => { mockUseGroupedModelIds.mockReturnValue({ preferredModelIds: ["preferred-1"], restModelIds: [], - hasPreferred: true, }) mockUseProviderModels.mockReturnValue({ diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index 5eecd933a82..9c1c933f771 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -88,7 +88,7 @@ export const ModelPicker = ({ const closeTimeoutRef = useRef(null) // kilocode_change start: Use grouped model IDs for section headers - const { preferredModelIds, restModelIds, hasPreferred } = useGroupedModelIds(models) + const { preferredModelIds, restModelIds } = useGroupedModelIds(models) const modelIds = useMemo(() => [...preferredModelIds, ...restModelIds], [preferredModelIds, restModelIds]) const [isPricingExpanded, setIsPricingExpanded] = useState(false) // kilocode_change end @@ -206,7 +206,7 @@ export const ModelPicker = ({ )} {/* kilocode_change start: Section headers for recommended and all models */} - {hasPreferred && preferredModelIds.length > 0 && ( + {preferredModelIds.length > 0 && ( {preferredModelIds.map((model) => ( { expect(content).toBeInTheDocument() }) - // kilocode_change start: Tests for LABEL type (section headers) - it("renders label options as section headers", () => { - const optionsWithLabel = [ - { value: "__label_recommended__", label: "Recommended models", type: DropdownOptionType.LABEL }, - { value: "model1", label: "Model 1" }, - { value: "__label_all__", label: "All models", type: DropdownOptionType.LABEL }, - { value: "model2", label: "Model 2" }, - ] - - render() - - // Click the trigger to open the dropdown - const trigger = screen.getByTestId("dropdown-trigger") - fireEvent.click(trigger) - - // Now we can check for dropdown content - const content = screen.getByTestId("dropdown-content") - expect(content).toBeInTheDocument() - }) - - it("LABEL options are non-selectable", () => { - const optionsWithLabel = [ - { value: "__label_section__", label: "Section Header", type: DropdownOptionType.LABEL }, - { value: "option1", label: "Option 1" }, - ] - - render() - - // Click the trigger to open the dropdown - const trigger = screen.getByTestId("dropdown-trigger") - fireEvent.click(trigger) - - // The content should be rendered - const content = screen.getByTestId("dropdown-content") - expect(content).toBeInTheDocument() - - // Note: The actual component renders LABEL items as static divs without onClick handlers - // (with data-testid="dropdown-label"), making them inherently non-selectable. - // This test uses mocks, so we just verify the component renders correctly. - // The non-selectability behavior is ensured by the component's implementation. - }) - // kilocode_change end - it("calls onChange for regular menu items", () => { render() diff --git a/webview-ui/src/components/ui/hooks/kilocode/__tests__/usePreferredModels.spec.ts b/webview-ui/src/components/ui/hooks/kilocode/__tests__/usePreferredModels.spec.ts index 7a7373f6aa2..7422f1d2614 100644 --- a/webview-ui/src/components/ui/hooks/kilocode/__tests__/usePreferredModels.spec.ts +++ b/webview-ui/src/components/ui/hooks/kilocode/__tests__/usePreferredModels.spec.ts @@ -20,7 +20,6 @@ describe("getGroupedModelIds", () => { expect(result).toEqual({ preferredModelIds: [], restModelIds: [], - hasPreferred: false, }) }) @@ -30,7 +29,6 @@ describe("getGroupedModelIds", () => { expect(result).toEqual({ preferredModelIds: [], restModelIds: [], - hasPreferred: false, }) }) @@ -46,7 +44,6 @@ describe("getGroupedModelIds", () => { expect(result.preferredModelIds).toEqual(["model-c", "model-a"]) expect(result.restModelIds).toEqual(["model-b", "model-d"]) - expect(result.hasPreferred).toBe(true) }) it("sorts preferred models by preferredIndex", () => { @@ -59,7 +56,6 @@ describe("getGroupedModelIds", () => { const result = getGroupedModelIds(models) expect(result.preferredModelIds).toEqual(["model-a", "model-m", "model-z"]) - expect(result.hasPreferred).toBe(true) }) it("sorts rest models alphabetically", () => { @@ -72,7 +68,6 @@ describe("getGroupedModelIds", () => { const result = getGroupedModelIds(models) expect(result.restModelIds).toEqual(["alpha-model", "beta-model", "zebra-model"]) - expect(result.hasPreferred).toBe(false) }) it("handles case where all models are preferred", () => { @@ -85,7 +80,6 @@ describe("getGroupedModelIds", () => { expect(result.preferredModelIds).toEqual(["model-a", "model-b"]) expect(result.restModelIds).toEqual([]) - expect(result.hasPreferred).toBe(true) }) it("handles case where no models are preferred", () => { @@ -98,12 +92,11 @@ describe("getGroupedModelIds", () => { expect(result.preferredModelIds).toEqual([]) expect(result.restModelIds).toEqual(["model-a", "model-b"]) - expect(result.hasPreferred).toBe(false) }) }) describe("useGroupedModelIds", () => { - it("returns grouped model IDs with hasPreferred flag", () => { + it("returns grouped model IDs", () => { const models: Record = { "pref-model": createModelInfo({ preferredIndex: 0 }), "rest-model": createModelInfo(), @@ -113,7 +106,6 @@ describe("useGroupedModelIds", () => { expect(result.current.preferredModelIds).toEqual(["pref-model"]) expect(result.current.restModelIds).toEqual(["rest-model"]) - expect(result.current.hasPreferred).toBe(true) }) it("memoizes result when models don't change", () => { diff --git a/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts b/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts index 92ee7c3b10a..6d9e6ac807d 100644 --- a/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts +++ b/webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts @@ -1,17 +1,16 @@ import { useMemo } from "react" import type { ModelInfo } from "@roo-code/types" -// Result containing preferred and rest model IDs, plus a flag indicating if there are preferred models +// Result containing preferred and rest model IDs export interface GroupedModelIds { preferredModelIds: string[] restModelIds: string[] - hasPreferred: boolean } // Extracts and groups model IDs into preferred and rest categories export const getGroupedModelIds = (models: Record | null): GroupedModelIds => { if (!models) { - return { preferredModelIds: [], restModelIds: [], hasPreferred: false } + return { preferredModelIds: [], restModelIds: [] } } const preferredModelIds: string[] = [] @@ -41,7 +40,6 @@ export const getGroupedModelIds = (models: Record | null): Gr return { preferredModelIds, restModelIds, - hasPreferred: preferredModelIds.length > 0, } } diff --git a/webview-ui/src/components/ui/select-dropdown.tsx b/webview-ui/src/components/ui/select-dropdown.tsx index 09b8a622e25..78372b55682 100644 --- a/webview-ui/src/components/ui/select-dropdown.tsx +++ b/webview-ui/src/components/ui/select-dropdown.tsx @@ -319,7 +319,7 @@ export const SelectDropdown = React.memo( if (option.type === DropdownOptionType.LABEL) { return (
{option.label} From 46d16a5d11bfc59bfcbacaf202cacb572f959b66 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 9 Jan 2026 11:34:52 +0100 Subject: [PATCH 09/10] docs: add Project Structure and Build Commands sections to AGENTS.md (#4893) --- AGENTS.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index da2137db93d..e7ca48bdc38 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,31 @@ Kilo Code is an open source AI coding agent for VS Code that generates code from natural language, automates tasks, and supports 500+ AI models. +## Project Structure + +This is a pnpm monorepo using Turbo for task orchestration: + +- **`src/`** - VSCode extension (core logic, API providers, tools) +- **`webview-ui/`** - React frontend (chat UI, settings) +- **`cli/`** - Standalone CLI package +- **`packages/`** - Shared packages (`types`, `ipc`, `telemetry`, `cloud`) +- **`jetbrains/`** - JetBrains plugin (Kotlin + Node.js host) +- **`apps/`** - E2E tests, Storybook, docs + +Key source directories: +- `src/api/providers/` - AI provider implementations (50+ providers) +- `src/core/tools/` - Tool implementations (ReadFile, ApplyDiff, ExecuteCommand, etc.) +- `src/services/` - Services (MCP, browser, checkpoints, code-index) + +## Build Commands + +```bash +pnpm install # Install all dependencies +pnpm build # Build extension (.vsix) +pnpm lint # Run ESLint +pnpm check-types # TypeScript type checking +``` + ## Skills - **Translation**: `.kilocode/skills/translation/SKILL.md` - Translation and localization guidelines From b37c944a8bea644660b6f2c4400d0b47cbdee979 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 9 Jan 2026 11:35:07 +0100 Subject: [PATCH 10/10] fix: agent manager session disappears due to gitUrl race condition (#4892) --- .../fix-agent-manager-session-disappears.md | 5 +++++ .../agent-manager/AgentManagerProvider.ts | 6 +++++ .../__tests__/AgentManagerProvider.spec.ts | 22 +++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 .changeset/fix-agent-manager-session-disappears.md diff --git a/.changeset/fix-agent-manager-session-disappears.md b/.changeset/fix-agent-manager-session-disappears.md new file mode 100644 index 00000000000..b02d60a95a4 --- /dev/null +++ b/.changeset/fix-agent-manager-session-disappears.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix Agent Manager session disappearing immediately after starting due to gitUrl race condition diff --git a/src/core/kilocode/agent-manager/AgentManagerProvider.ts b/src/core/kilocode/agent-manager/AgentManagerProvider.ts index b508c46e4c4..36ca0b82bf8 100644 --- a/src/core/kilocode/agent-manager/AgentManagerProvider.ts +++ b/src/core/kilocode/agent-manager/AgentManagerProvider.ts @@ -473,6 +473,12 @@ export class AgentManagerProvider implements vscode.Disposable { if (workspaceFolder) { try { gitUrl = normalizeGitUrl(await getRemoteUrl(workspaceFolder)) + // Update currentGitUrl to ensure consistency between session gitUrl and filter + // This fixes a race condition where initializeCurrentGitUrl() hasn't completed yet + if (gitUrl && !this.currentGitUrl) { + this.currentGitUrl = gitUrl + this.outputChannel.appendLine(`[AgentManager] Updated current git URL: ${gitUrl}`) + } } catch (error) { this.outputChannel.appendLine( `[AgentManager] Could not get git URL: ${error instanceof Error ? error.message : String(error)}`, diff --git a/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts b/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts index dbc4234e8fb..b3f97af87ba 100644 --- a/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts +++ b/src/core/kilocode/agent-manager/__tests__/AgentManagerProvider.spec.ts @@ -842,6 +842,28 @@ describe("AgentManagerProvider gitUrl filtering", () => { expect(state.sessions[0].sessionId).toBe("session-2") }) + it("updates currentGitUrl when starting a session if not already set (race condition fix)", async () => { + // Simulate the race condition: currentGitUrl is undefined because initializeCurrentGitUrl hasn't completed + ;(provider as any).currentGitUrl = undefined + + // Start a session - this should update currentGitUrl + await (provider as any).startAgentSession("test prompt") + + // currentGitUrl should now be set from the session's gitUrl + expect((provider as any).currentGitUrl).toBe("https://github.com/org/repo.git") + }) + + it("does not overwrite currentGitUrl if already set", async () => { + // Set a different currentGitUrl + ;(provider as any).currentGitUrl = "https://github.com/org/other-repo.git" + + // Start a session + await (provider as any).startAgentSession("test prompt") + + // currentGitUrl should NOT be overwritten + expect((provider as any).currentGitUrl).toBe("https://github.com/org/other-repo.git") + }) + describe("filterRemoteSessionsByGitUrl", () => { it("returns only sessions with matching git_url when currentGitUrl is set", () => { const remoteSessions = [