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

Filter by extension

Filter by extension

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

Open the model selector instantly, even with large model catalogs.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions packages/kilo-vscode/tests/model-selector-accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,24 @@ test("selected favorite remains selected when its duplicate group is collapsed",
await expect(combobox).toHaveAttribute("aria-activedescendant", await favorites.getAttribute("id"))
})

test("large catalogs keep the rendered tree bounded and navigate to distant models", async ({ page }) => {
await load(page, "shared--model-selector-large-catalog")

await page.getByRole("button", { name: "Select model: Provider 0 / Model 300" }).click()
const combobox = page.getByRole("combobox", { name: "Select model: Provider 0 / Model 300. Search models" })
const tree = page.getByRole("tree", { name: "Select model" })

// The window mounts before we measure it, yet stays far smaller than the catalog.
await expect.poll(() => tree.getByRole("treeitem").count()).toBeGreaterThan(0)
await expect.poll(() => tree.getByRole("treeitem").count()).toBeLessThan(50)

// Reaching a distant model scrolls it into the mounted window and activates it.
await combobox.fill("Model 599")
const last = page.getByRole("treeitem", { name: "Model 599" })
await expect(last).toBeVisible()
await expect(combobox).toHaveAttribute("aria-activedescendant", await last.getAttribute("id"))
})

test("Enter selects the active option and Escape restores selector focus", async ({ page }) => {
await load(page, "shared--model-selector-accessible")

Expand Down Expand Up @@ -252,6 +270,35 @@ test("settings and mode editing expose distinct model field purposes", async ({
)
})

test("mode picker focuses the selected mode as it opens", async ({ page }) => {
await load(page, "prompt-input--default-420")

await page.getByRole("button", { name: "Code", exact: true }).click()
await expect(page.locator(".mode-switcher-item.selected")).toBeFocused()
})

test("variant picker focuses the selected effort as it opens", async ({ page }) => {
await load(page, "prompt-input--with-thinking-420")

await page.getByRole("button", { name: "Medium", exact: true }).click()
await expect(page.locator(".thinking-selector-item.selected")).toBeFocused()
})

test("slash mode picker Escape returns focus to the prompt", async ({ page }) => {
await load(page, "prompt-input--default-420")

const prompt = page.locator("textarea.prompt-input")
await prompt.evaluate((el) => el.setAttribute("aria-disabled", "false"))
await prompt.fill("/agents")
await prompt.press("Enter")

const selected = page.locator(".mode-switcher-item.selected")
await expect(selected).toBeFocused()
await selected.press("Escape")

await expect(prompt).toBeFocused()
})

test("chat picker Escape returns focus to the prompt", async ({ page }) => {
await load(page, "prompt-input--default-420")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,15 @@ export const ModeSwitcherBase: Component<ModeSwitcherBaseProps> = (props) => {
const [focused, setFocused] = createSignal(-1)
const language = useLanguage()
let listRef: HTMLDivElement | undefined
// True while the picker was opened by the slash command rather than a click,
// so dismissal returns focus to the prompt like the model/variant pickers.
let slash = false

// Listen for slash command trigger
const onTrigger = () => setOpen(true)
const onTrigger = () => {
slash = true
openSelected()
}
window.addEventListener("openModePicker", onTrigger)
onCleanup(() => window.removeEventListener("openModePicker", onTrigger))

Expand All @@ -65,11 +71,23 @@ export const ModeSwitcherBase: Component<ModeSwitcherBaseProps> = (props) => {
items[clamped]?.focus()
}

function openSelected() {
const idx = props.agents.findIndex((a) => a.name === props.value)
setFocused(idx >= 0 ? idx : 0)
setOpen(true)
}

function onOpen(val: boolean) {
setOpen(val)
if (val) {
const idx = props.agents.findIndex((a) => a.name === props.value)
requestAnimationFrame(() => focusItem(idx >= 0 ? idx : 0))
// A click on the trigger opens without the slash flag.
slash = false
openSelected()
return
}
setOpen(false)
if (slash) {
slash = false
requestAnimationFrame(() => window.dispatchEvent(new CustomEvent("focusPrompt", { detail: { restore: true } })))
}
}

Expand Down Expand Up @@ -135,6 +153,7 @@ export const ModeSwitcherBase: Component<ModeSwitcherBaseProps> = (props) => {
role="option"
aria-selected={agent.name === props.value}
tabindex={focused() === i() ? 0 : -1}
data-autofocus={focused() === i() ? "" : undefined}
onClick={() => pick(agent.name)}
onFocus={() => setFocused(i())}
>
Expand Down
Loading
Loading