fix(vscode): preserve model preview interactions

This commit is contained in:
marius-kilocode
2026-05-29 09:32:07 +02:00
parent 38fcaa65e7
commit 96d2d3addb
3 changed files with 73 additions and 6 deletions
@@ -37,11 +37,28 @@ test("model selector exposes combobox relationships and active option movement",
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 }) => {
@@ -50,9 +67,11 @@ test("selected favorite remains selected when its duplicate group is collapsed",
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 page.getByRole("button", { name: "Collapse Favorites" }).click()
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"))
@@ -427,7 +427,10 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (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
@@ -720,7 +723,7 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
{(group) => {
const shown = () => isGroupOpen(group.key)
return (
<div role="group" aria-labelledby={groupID(group.key)}>
<div class="model-selector-group" role="group" aria-labelledby={groupID(group.key)}>
<button
id={groupID(group.key)}
type="button"
@@ -756,6 +759,7 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
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 (
@@ -772,7 +776,18 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
class={`model-selector-item${(hovered() && !pointer()) || preActive() ? " keyboard-focused" : ""}${hovered() || preActive() ? " selected" : ""}${chosen(row) ? " active" : ""}`}
role="option"
aria-selected={chosen(row)}
onClick={() => selectRow(row)}
onClick={() => {
if (!expanded()) {
selectRow(row)
return
}
setRow(row.key)
setPreviewKey(row.key)
searchRef?.focus()
}}
onDblClick={() => {
if (expanded()) selectRow(row)
}}
onMouseMove={() => {
setPointer(true)
}}
@@ -821,6 +836,16 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
<Icon name={starred() ? "star-filled" : "star"} size="small" />
</button>
</Show>
<Show when={showSelect()}>
<button
type="button"
class="model-selector-item-select-btn"
aria-label={`${language.t("dialog.model.select")}: ${sanitizeName(model.name)}`}
onClick={() => selectRow(row)}
>
{language.t("dialog.model.select")}
</button>
</Show>
</div>
)
}}
@@ -841,7 +866,9 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
class={`model-selector-preview${expanded() ? " model-selector-preview--visible" : ""}`}
style={expanded() ? { height: `${previewHeight()}px` } : {}}
>
<ModelPreview model={previewModel() ?? activeModel() ?? null} />
<Show when={expanded()}>
<ModelPreview model={previewModel() ?? activeModel() ?? null} />
</Show>
</div>
</div>
)
@@ -292,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;
@@ -445,6 +445,27 @@
opacity: 1;
}
.model-selector-item-select-btn {
flex-shrink: 0;
margin-right: 12px;
padding: 2px 10px;
font-size: var(--kilo-font-size-11);
font-weight: 600;
font-family: inherit;
border-radius: 2px;
border: none;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
opacity: 0.95;
cursor: pointer;
line-height: 1.6;
letter-spacing: 0.02em;
}
.model-selector-item-select-btn:hover {
opacity: 1;
}
/* ============================================
Thinking Selector (uses kilo-ui Popover)
============================================ */