diff --git a/.changeset/clear-model-navigation.md b/.changeset/clear-model-navigation.md new file mode 100644 index 00000000000..932636c1a70 --- /dev/null +++ b/.changeset/clear-model-navigation.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Make model selection in chat and settings operable with screen readers by announcing searchable options, keyboard navigation, selected values, and model-setting purpose. diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-accessible-labels-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-accessible-labels-chromium-linux.png new file mode 100644 index 00000000000..01ea6e7ac02 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-accessible-labels-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbfed6606c5e1ccd6a744bdadf34ee597007d5f82c436c8c53e189560e0837d4 +size 30244 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-accessible-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-accessible-chromium-linux.png new file mode 100644 index 00000000000..f6894817821 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-accessible-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89fa6a619fc2089fd0bb03dabe191394d3dcf3423ba066f1e8f2e2c6aac38838 +size 1085 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-selected-favorite-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-selected-favorite-chromium-linux.png new file mode 100644 index 00000000000..f6894817821 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-selected-favorite-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89fa6a619fc2089fd0bb03dabe191394d3dcf3423ba066f1e8f2e2c6aac38838 +size 1085 diff --git a/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts b/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts new file mode 100644 index 00000000000..dd634e67ff9 --- /dev/null +++ b/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts @@ -0,0 +1,151 @@ +import { expect, test, type Page } from "@playwright/test" + +const GLOBALS = "colorScheme:dark;theme:kilo-vscode;vscodeTheme:dark-modern" + +function story(id: string) { + return `/iframe.html?id=${id}&viewMode=story&globals=${GLOBALS}` +} + +async function load(page: Page, id: string) { + await page.goto(story(id), { waitUntil: "load" }) + await page.waitForSelector("#storybook-root *", { state: "attached" }) +} + +test("model selector exposes combobox relationships and active option movement", async ({ page }) => { + await load(page, "shared--model-selector-accessible") + + await page.getByRole("button", { name: "Review model: Alpha" }).click() + const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" }) + const listbox = page.getByRole("listbox", { name: "Review model" }) + const alpha = page.getByRole("option", { name: "Alpha" }) + const bravo = page.getByRole("option", { name: "Bravo" }) + + await expect(combobox).toBeFocused() + await expect(combobox).toHaveAttribute("aria-expanded", "true") + await expect(combobox).toHaveAttribute("aria-controls", await listbox.getAttribute("id")) + await expect(combobox).toHaveAttribute("aria-activedescendant", await alpha.getAttribute("id")) + await expect(combobox).toHaveAccessibleDescription("Choose the model used for code review tasks.") + await expect(alpha.locator("button")).toHaveCount(0) + await expect(page.getByRole("button", { name: "Add to favorites: Alpha" })).toBeVisible() + + await combobox.press("ArrowDown") + await expect(combobox).toBeFocused() + await expect(combobox).toHaveAttribute("aria-activedescendant", await bravo.getAttribute("id")) + + const expand = page.getByRole("button", { name: "Expand" }) + const controls = await expand.getAttribute("aria-controls") + const preview = page.locator(`[id="${controls}"]`) + await expect(expand).toHaveAttribute("aria-expanded", "false") + await expect(preview).toHaveAttribute("aria-hidden", "true") + await expect(preview.locator("button, a, [tabindex]")).toHaveCount(0) + await expand.click() + const collapse = page.getByRole("button", { name: "Collapse", exact: true }) + await expect(collapse).toHaveAttribute("aria-controls", controls!) + await expect(collapse).toHaveAttribute("aria-expanded", "true") + await expect(preview).toHaveAttribute("aria-hidden", "false") + await expect(preview.getByRole("button", { name: "Add to favorites" })).toBeVisible() +}) + +test("expanded preview waits for explicit pointer selection", async ({ page }) => { + await load(page, "shared--model-selector-accessible") + + await page.getByRole("button", { name: "Review model: Alpha" }).click() + await page.getByRole("button", { name: "Expand" }).click() + await page.getByRole("option", { name: "Bravo" }).click() + + await expect(page.getByTestId("model-selector-value")).toHaveText("alpha") + await expect(page.getByRole("combobox", { name: "Review model: Alpha. Search models" })).toBeVisible() + await expect(page.locator(".model-selector-preview")).toContainText("Bravo") + + await page.getByRole("button", { name: "Select: Bravo" }).click() + await expect(page.getByTestId("model-selector-value")).toHaveText("bravo") +}) + +test("selected favorite remains selected when its duplicate group is collapsed", async ({ page }) => { + await load(page, "shared--model-selector-selected-favorite") + + await page.getByRole("button", { name: "Review model: Alpha" }).click() + const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" }) + const alpha = page.getByRole("option", { name: "Alpha" }) + const favorites = page.getByRole("button", { name: "Collapse Favorites" }) + await expect(alpha.first()).toHaveAttribute("aria-selected", "true") + await expect.poll(() => favorites.evaluate((el) => getComputedStyle(el).borderTopStyle)).toBe("solid") + + await favorites.click() + await expect(alpha).toHaveCount(1) + await expect(alpha).toHaveAttribute("aria-selected", "true") + await expect(combobox).toHaveAttribute("aria-activedescendant", await alpha.getAttribute("id")) +}) + +test("Enter selects the active option and Escape restores selector focus", async ({ page }) => { + await load(page, "shared--model-selector-accessible") + + await page.getByRole("button", { name: "Review model: Alpha" }).click() + const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" }) + await combobox.press("ArrowDown") + await combobox.press("Enter") + + const trigger = page.getByRole("button", { name: "Review model: Bravo" }) + await expect(page.getByTestId("model-selector-value")).toHaveText("bravo") + await expect(trigger).toBeFocused() + + await trigger.click() + const reopened = page.getByRole("combobox", { name: "Review model: Bravo. Search models" }) + await reopened.press("ArrowDown") + await reopened.press("Escape") + + await expect(page.getByTestId("model-selector-value")).toHaveText("bravo") + await expect(trigger).toBeFocused() +}) + +test("no-match search announces the empty result and can choose the default option", async ({ page }) => { + await load(page, "shared--model-selector-accessible") + + await page.getByRole("button", { name: "Review model: Alpha" }).click() + const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" }) + await combobox.fill("no matching model") + + await expect(page.locator(".model-selector-empty")).toHaveText("No model results") + const clear = page.getByRole("option", { name: "Use default model" }) + await expect(combobox).toHaveAttribute("aria-activedescendant", await clear.getAttribute("id")) + await combobox.press("Enter") + + await expect(page.getByTestId("model-selector-value")).toHaveText("default") + await expect(page.getByRole("button", { name: "Review model: Use default model" })).toBeFocused() +}) + +test("settings and mode editing expose distinct model field purposes", async ({ page }) => { + await load(page, "settings--models-accessible-labels") + + await expect(page.getByRole("button", { name: "Default Model: Not set" })).toHaveAccessibleDescription( + "Primary model for conversations", + ) + await expect(page.getByRole("button", { name: "Small Model: Not set" })).toHaveAccessibleDescription( + /Lightweight model/, + ) + await expect(page.getByRole("button", { name: "Subagent Model: Not set" })).toHaveAccessibleDescription( + /Default model and reasoning effort/, + ) + await expect(page.getByRole("button", { name: "Autocomplete model: Not set" })).toHaveAccessibleDescription( + "Select the model used for inline code completions", + ) + await expect(page.getByRole("button", { name: "Model per Mode: code: Not set" })).toHaveAccessibleDescription( + /Override the default model for specific modes/, + ) + + await load(page, "settings--mode-edit-export") + await expect(page.getByRole("button", { name: /Model Override:/ })).toHaveAccessibleDescription( + "Override the default model for this agent", + ) +}) + +test("chat picker Escape returns focus to the prompt", async ({ page }) => { + await load(page, "prompt-input--default-420") + + await page.getByRole("button", { name: /^Select model:/ }).click() + const combobox = page.getByRole("combobox", { name: /^Select model:.*Search models$/ }) + await expect(combobox).toBeFocused() + await combobox.press("Escape") + + await expect(page.locator("textarea.prompt-input")).toBeFocused() +}) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ModeEditView.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ModeEditView.tsx index 12ba482b3eb..159ef186272 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ModeEditView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ModeEditView.tsx @@ -180,6 +180,8 @@ const ModeEditView: Component = (props) => { placement="bottom-start" allowClear clearLabel={language.t("settings.providers.notSet")} + label={language.t("settings.agentBehaviour.modelOverride.title")} + description={language.t("settings.agentBehaviour.modelOverride.description")} /> diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx index c01e0434094..a3fc83d1c21 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx @@ -101,6 +101,8 @@ const ModelsTab: Component = () => { placement="bottom-start" allowClear clearLabel={language.t("settings.providers.notSet")} + label={language.t("settings.providers.defaultModel.title")} + description={language.t("settings.providers.defaultModel.description")} /> { allowClear clearLabel={language.t("settings.providers.notSet")} includeAutoSmall + label={language.t("settings.providers.smallModel.title")} + description={language.t("settings.providers.smallModel.description")} /> { placement="bottom-start" allowClear clearLabel={language.t("settings.providers.notSet")} + label={language.t("settings.providers.subagentModel.title")} + description={language.t("settings.providers.subagentModel.description")} /> { favorites={false} allowClear clearLabel={language.t("settings.providers.notSet")} + label={language.t("settings.autocomplete.model.title")} + description={language.t("settings.autocomplete.model.description")} /> @@ -167,6 +175,8 @@ const ModelsTab: Component = () => { placement="bottom-start" allowClear clearLabel={language.t("settings.providers.notSet")} + label={`${language.t("settings.providers.modeModels")}: ${agent.name}`} + description={language.t("settings.providers.modeModels.description")} /> )} diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx index 98698a5a6d7..4ae79db99d6 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx @@ -7,7 +7,18 @@ * ModelSelector — thin wrapper wired to session context for chat usage. */ -import { createSignal, createMemo, createEffect, onCleanup, For, Show, createSelector, useContext } from "solid-js" +import { + createSignal, + createMemo, + createEffect, + createUniqueId, + onCleanup, + For, + Show, + createSelector, + useContext, + untrack, +} from "solid-js" import type { Accessor, Component } from "solid-js" import { PopupSelector } from "./PopupSelector" import { Button } from "@kilocode/kilo-ui/button" @@ -98,6 +109,10 @@ export interface ModelSelectorBaseProps { deferDismiss?: boolean /** Render inline instead of through a portal when nested in a dialog. */ portal?: boolean + /** Accessible purpose of this model setting or selector. */ + label?: string + /** Additional accessible context for this model setting. */ + description?: string } export const ModelSelectorBase: Component = (props) => { @@ -106,6 +121,12 @@ export const ModelSelectorBase: Component = (props) => { // Session context is optional — ModelSelectorBase is also used in Settings // where SessionProvider may not be mounted. const session = useContext(SessionContext) + const uid = createUniqueId() + const listID = `${uid}-models` + const previewID = `${uid}-preview` + const descriptionID = `${uid}-description` + const optionID = (key: string) => `${uid}-option-${encodeURIComponent(key)}` + const groupID = (key: string) => `${uid}-group-${encodeURIComponent(key)}` const activeModel = () => { const items = props.models if (items) return items.find((m) => m.providerID === props.value?.providerID && m.id === props.value?.modelID) @@ -312,9 +333,16 @@ export const ModelSelectorBase: Component = (props) => { const activeKey = (m?: EnrichedModel | null) => { if (!m) return props.allowClear ? CLEAR_KEY : defaultKey() const key = modelKey(m.providerID, m.id) - if (!debouncedSearch() && favoriteKeys().has(key)) return favoriteKey(m) + const favorite = favoriteKey(m) + if (!debouncedSearch() && favoriteKeys().has(key) && rowMap().has(favorite)) return favorite return canonicalKey(m) } + const chosen = (row: ModelRow) => { + if (row.kind === "clear") return !props.value?.providerID + if (!row.model || !isActive(row.model)) return false + return activeKey(row.model) === row.key + } + const activeOptionID = () => (rowMap().has(selectedKey()) ? optionID(selectedKey()) : undefined) const [anchor, setAnchor] = createSignal(null) const previewModel = createMemo(() => rowMap().get(previewKey() ?? "")?.model ?? null) @@ -329,7 +357,11 @@ export const ModelSelectorBase: Component = (props) => { // which resets selection. createEffect(() => { rows() // track - setSelectedKey((prev) => (rowMap().has(prev) ? prev : defaultKey())) + setSelectedKey((prev) => { + if (rowMap().has(prev)) return prev + const next = untrack(() => activeKey(activeModel())) + return rowMap().has(next) ? next : defaultKey() + }) setPreActiveKey((prev) => (prev && rowMap().has(prev) ? prev : null)) setPreviewKey((prev) => (prev && rowMap().has(prev) ? prev : null)) }) @@ -395,7 +427,10 @@ export const ModelSelectorBase: Component = (props) => { // Listen for slash command trigger const onTrigger = () => setOpen(true) window.addEventListener("openModelPicker", onTrigger) - onCleanup(() => window.removeEventListener("openModelPicker", onTrigger)) + onCleanup(() => { + window.removeEventListener("openModelPicker", onTrigger) + clearTimeout(previewTimer) + }) const onEscape = (e: KeyboardEvent) => { if (!open() || e.key !== "Escape") return @@ -545,247 +580,301 @@ export const ModelSelectorBase: Component = (props) => { notSet: language.t("dialog.model.notSet"), }, ) + const label = () => props.label ?? language.t("dialog.model.select.title") + const controlLabel = () => `${label()}: ${triggerLabel()}` + const searchLabel = () => `${controlLabel()}. ${language.t("dialog.model.search.placeholder")}` + const describedBy = () => (props.description ? descriptionID : undefined) return ( - - {triggerLabel()} - - - - - } - class={`model-selector-popover${expanded() ? " model-selector-popover--expanded" : ""}`} - > - {(bodyH) => { - createEffect(() => { - if (!expanded()) return - const h = bodyH() - if (h === undefined) return - const chrome = (searchWrapperRef?.offsetHeight ?? 0) + (splitterRef?.offsetHeight ?? 0) - setPreviewHeight((h - chrome) / 2) - }) - return ( -
-
- setSearch(e.currentTarget.value)} - /> - - { - setExpanded((v) => { - if (v) { - setPreActiveKey(null) - setPreviewKey(null) - } - return !v - }) - requestAnimationFrame(() => { - searchRef?.focus() - scrollRow(preActiveKey() ?? selectedKey(), "nearest") - }) - }} + <> + + + {props.description} + + + + {triggerLabel()} + + + + + } + class={`model-selector-popover${expanded() ? " model-selector-popover--expanded" : ""}`} + > + {(bodyH) => { + createEffect(() => { + if (!expanded()) return + const h = bodyH() + if (h === undefined) return + const chrome = (searchWrapperRef?.offsetHeight ?? 0) + (splitterRef?.offsetHeight ?? 0) + setPreviewHeight((h - chrome) / 2) + }) + return ( +
+
+ setSearch(e.currentTarget.value)} /> - -
- -
- -
{language.t("dialog.model.empty")}
-
- - -
pickClear()} - onMouseMove={() => { - setPointer(true) - }} - onMouseEnter={() => { - if (pointer()) setSelectedKey(CLEAR_KEY) - }} + - - {props.clearLabel ?? language.t("dialog.model.notSet")} - -
-
+ { + setExpanded((v) => { + if (v) { + setPreActiveKey(null) + setPreviewKey(null) + } + return !v + }) + requestAnimationFrame(() => { + searchRef?.focus() + scrollRow(preActiveKey() ?? selectedKey(), "nearest") + }) + }} + /> + +
- - {(group) => { - const shown = () => isGroupOpen(group.key) - return ( - <> - - - - {(row) => { - if (!row.model) return null - const model = row.model - const hovered = () => isSelected(row.key) - const preActive = () => isPreActive(row.key) - const showSelectBtn = () => expanded() && preActive() && !isActive(model) - const starred = () => favoriteKeys().has(modelKey(model.providerID, model.id)) - const showProvider = () => row.kind === "favorite" - return ( -
{ - refs.set(row.key, el) - onCleanup(() => refs.delete(row.key)) - }} - class={`model-selector-item${(hovered() && !pointer()) || preActive() ? " keyboard-focused" : ""}${hovered() || preActive() ? " selected" : ""}${isActive(model) && row.kind === "model" ? " active" : ""}`} - role="option" - aria-selected={isActive(model) && row.kind === "model"} - onClick={() => { - setRow(row.key) - setPreviewKey(row.key) - if (!expanded()) selectRow(row) - searchRef?.focus() - }} - onDblClick={() => { - if (expanded()) selectRow(row) - }} - onMouseMove={() => { - setPointer(true) - }} - onMouseEnter={() => { - if (pointer()) setSelectedKey(row.key) - }} - > -
- - {(() => { - const full = sanitizeName(model.name) - const sep = full.indexOf(": ") - if (sep < 0) return {full} - return ( - <> - {full.slice(0, sep)} - {full.slice(sep + 2)} - - ) - })()} - - - {language.t("model.tag.free")} + + {group.label} + + + + + + {(row) => { + if (!row.model) return null + const model = row.model + const hovered = () => isSelected(row.key) + const preActive = () => isPreActive(row.key) + const starred = () => favoriteKeys().has(modelKey(model.providerID, model.id)) + const showProvider = () => row.kind === "favorite" + const showSelect = () => expanded() && preActive() && !isActive(model) + const starLabel = () => + `${starred() ? language.t("model.favorite.remove") : language.t("model.favorite.add")}: ${sanitizeName(model.name)}` + return ( + - - - - - - -
- ) - }} - - - - ) - }} - -
+ ) + }} +
+
+
+ ) + }} + +
- -
- -
- + +
+ +
+ + + +
-
- ) - }} - + ) + }} + + ) } diff --git a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx index 6d4cf57a7a3..8fdfc3cd4e0 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx @@ -71,6 +71,17 @@ export const ModelsAutocompleteOpen: Story = { ), } +export const ModelsAccessibleLabels: Story = { + name: "ModelsTab — accessible model labels", + render: () => ( + +
+ +
+
+ ), +} + function OpenModelPicker(props: { children: any }) { let ref: HTMLDivElement | undefined onMount(() => { diff --git a/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx index 18f48f1db9c..7474b0809a4 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx @@ -3,9 +3,13 @@ * Stories for shared controls: ModelSelector. */ +import { createSignal } from "solid-js" import type { Meta, StoryObj } from "storybook-solidjs-vite" -import { StoryProviders } from "./StoryProviders" +import { StoryProviders, mockSessionValue } from "./StoryProviders" import { ModelSelectorBase } from "../components/shared/ModelSelector" +import { SessionContext } from "../context/session" +import type { EnrichedModel } from "../context/provider" +import type { ModelSelection } from "../types/messages" const meta: Meta = { title: "Shared", @@ -32,3 +36,58 @@ export const ModelSelectorNoProviders: Story = { ), } + +const ACCESSIBLE_MODELS: EnrichedModel[] = [ + { id: "alpha", name: "Alpha", providerID: "kilo", providerName: "Kilo" }, + { id: "bravo", name: "Bravo", providerID: "kilo", providerName: "Kilo" }, + { id: "charlie", name: "Charlie", providerID: "kilo", providerName: "Kilo" }, +] + +const AccessibleModelSelector = () => { + const [value, setValue] = createSignal({ providerID: "kilo", modelID: "alpha" }) + + return ( +
+ { + setValue(providerID && modelID ? { providerID, modelID } : null) + }} + /> + {value()?.modelID ?? "default"} +
+ ) +} + +export const ModelSelectorAccessible: Story = { + name: "ModelSelector — accessible interaction", + render: () => ( + + + + ), +} + +export const ModelSelectorSelectedFavorite: Story = { + name: "ModelSelector — selected favorite", + render: () => { + const session = { + ...mockSessionValue(), + favoriteModels: () => [{ providerID: "kilo", modelID: "alpha" }], + } + + return ( + + + + + + ) + }, +} diff --git a/packages/kilo-vscode/webview-ui/src/styles/model-selector.css b/packages/kilo-vscode/webview-ui/src/styles/model-selector.css index cc8c23f0395..adecd84ceae 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/model-selector.css +++ b/packages/kilo-vscode/webview-ui/src/styles/model-selector.css @@ -207,6 +207,18 @@ flex-shrink: 0; } +.model-selector-assistive { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + .model-selector-search-wrapper { display: flex; align-items: center; @@ -280,7 +292,7 @@ outline-offset: -1px; } -.model-selector-group-label:not(:first-child) { +.model-selector-group:not(:first-child) > .model-selector-group-label { border-top: 1px solid var(--border-weak-base, var(--vscode-panel-border)); margin-top: 4px; padding-top: 8px; @@ -305,10 +317,17 @@ margin-left: 2px; } +.model-selector-row { + display: flex; + align-items: center; +} + .model-selector-item { display: flex; align-items: center; gap: 6px; + flex: 1; + min-width: 0; padding: 5px 12px; font-size: var(--kilo-font-size-12); cursor: pointer; @@ -399,12 +418,16 @@ color 0.1s ease; } +.model-selector-row > .model-selector-star { + margin-right: 12px; +} + .model-selector-star [data-component="icon"] { color: inherit; } -.model-selector-item:hover .model-selector-star, -.model-selector-item.selected .model-selector-star, +.model-selector-row:hover .model-selector-star, +.model-selector-row.selected .model-selector-star, .model-selector-star--active { opacity: 1; } @@ -424,6 +447,7 @@ .model-selector-item-select-btn { flex-shrink: 0; + margin-right: 12px; padding: 2px 10px; font-size: var(--kilo-font-size-11); font-weight: 600; @@ -442,11 +466,6 @@ opacity: 1; } -.model-selector-item-select-btn--hidden { - opacity: 0; - pointer-events: none; -} - /* ============================================ Thinking Selector (uses kilo-ui Popover) ============================================ */