mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
feat(vscode): make model picker sections collapsible
Click any section header (Favorites, Recommended, or a provider) to collapse it and skip an entire provider with one click. Search ignores collapse state. Collapse state resets each time the picker opens.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Make sections in the model picker collapsible. Click any section header (Favorites, Recommended, or a provider like Kilo Gateway) to hide its models. Collapse state resets each time the picker opens.
|
||||
@@ -120,6 +120,9 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
const [preActiveKey, setPreActiveKey] = createSignal<string | null>(null)
|
||||
const [previewKey, setPreviewKey] = createSignal<string | null>(null)
|
||||
const [previewHeight, setPreviewHeight] = createSignal(500)
|
||||
// Per-group collapse state. Not persisted — resets every time the
|
||||
// selector mounts so groups are always expanded on reopen.
|
||||
const [collapsed, setCollapsed] = createSignal<Set<string>>(new Set())
|
||||
// Snapshot of the active model key captured when the popover opens.
|
||||
// Used to reorder favorites so the current model appears first — but only
|
||||
// based on the state at open-time, not reactively, to avoid list jumps
|
||||
@@ -281,8 +284,23 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
return [...result, ...rest]
|
||||
})
|
||||
|
||||
// While a search is active, collapse state is ignored — otherwise users
|
||||
// get confusing "no results" when their match lives inside a collapsed group.
|
||||
const isGroupOpen = (key: string) => !!debouncedSearch() || !collapsed().has(key)
|
||||
|
||||
function toggleGroup(key: string) {
|
||||
setCollapsed((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(key)) next.delete(key)
|
||||
else next.add(key)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const rows = createMemo<ModelRow[]>(() => {
|
||||
const list = groups().flatMap((g) => g.rows)
|
||||
const search = !!debouncedSearch()
|
||||
const c = collapsed()
|
||||
const list = groups().flatMap((g) => (search || !c.has(g.key) ? g.rows : []))
|
||||
if (!props.allowClear) return list
|
||||
return [{ key: CLEAR_KEY, kind: "clear" }, ...list]
|
||||
})
|
||||
@@ -633,97 +651,123 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
</Show>
|
||||
|
||||
<For each={groups()}>
|
||||
{(group) => (
|
||||
<>
|
||||
<div class="model-selector-group-label">{group.label}</div>
|
||||
<For each={group.rows}>
|
||||
{(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 (
|
||||
<div
|
||||
ref={(el) => {
|
||||
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)
|
||||
}}
|
||||
>
|
||||
<div class="model-selector-item-left">
|
||||
<span class="model-selector-item-name">
|
||||
{(() => {
|
||||
const full = sanitizeName(model.name)
|
||||
const sep = full.indexOf(": ")
|
||||
if (sep < 0) return <span class="model-selector-item-name-main">{full}</span>
|
||||
return (
|
||||
<>
|
||||
<span class="model-selector-item-name-provider">{full.slice(0, sep)}</span>
|
||||
<span class="model-selector-item-name-main">{full.slice(sep + 2)}</span>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</span>
|
||||
<Show when={isFree(model)}>
|
||||
<Tag data-variant="member">{language.t("model.tag.free")}</Tag>
|
||||
</Show>
|
||||
<Show when={showProvider()}>
|
||||
<span class="model-selector-item-provider-tag">{model.providerName}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={session && props.favorites !== false}>
|
||||
<button
|
||||
type="button"
|
||||
class={`model-selector-star${starred() ? " model-selector-star--active" : ""}`}
|
||||
aria-label={
|
||||
starred() ? language.t("model.favorite.remove") : language.t("model.favorite.add")
|
||||
}
|
||||
aria-pressed={starred()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleFavorite(model, row)
|
||||
{(group) => {
|
||||
const shown = () => isGroupOpen(group.key)
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
class="model-selector-group-label"
|
||||
aria-expanded={shown()}
|
||||
aria-label={language.t(shown() ? "model.group.collapse" : "model.group.expand", {
|
||||
group: group.label,
|
||||
})}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => toggleGroup(group.key)}
|
||||
>
|
||||
<svg
|
||||
class={`model-selector-group-chevron${shown() ? "" : " model-selector-group-chevron--collapsed"}`}
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M4 6l4 5 4-5H4z" />
|
||||
</svg>
|
||||
<span>{group.label}</span>
|
||||
</button>
|
||||
<Show when={shown()}>
|
||||
<For each={group.rows}>
|
||||
{(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 (
|
||||
<div
|
||||
ref={(el) => {
|
||||
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)
|
||||
}}
|
||||
>
|
||||
<Icon name={starred() ? "star-filled" : "star"} size="small" />
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={expanded()}>
|
||||
<button
|
||||
class={`model-selector-item-select-btn${showSelectBtn() ? "" : " model-selector-item-select-btn--hidden"}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
selectRow(row)
|
||||
}}
|
||||
>
|
||||
{language.t("dialog.model.select")}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</>
|
||||
)}
|
||||
<div class="model-selector-item-left">
|
||||
<span class="model-selector-item-name">
|
||||
{(() => {
|
||||
const full = sanitizeName(model.name)
|
||||
const sep = full.indexOf(": ")
|
||||
if (sep < 0) return <span class="model-selector-item-name-main">{full}</span>
|
||||
return (
|
||||
<>
|
||||
<span class="model-selector-item-name-provider">{full.slice(0, sep)}</span>
|
||||
<span class="model-selector-item-name-main">{full.slice(sep + 2)}</span>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</span>
|
||||
<Show when={isFree(model)}>
|
||||
<Tag data-variant="member">{language.t("model.tag.free")}</Tag>
|
||||
</Show>
|
||||
<Show when={showProvider()}>
|
||||
<span class="model-selector-item-provider-tag">{model.providerName}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={session && props.favorites !== false}>
|
||||
<button
|
||||
type="button"
|
||||
class={`model-selector-star${starred() ? " model-selector-star--active" : ""}`}
|
||||
aria-label={
|
||||
starred() ? language.t("model.favorite.remove") : language.t("model.favorite.add")
|
||||
}
|
||||
aria-pressed={starred()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleFavorite(model, row)
|
||||
}}
|
||||
>
|
||||
<Icon name={starred() ? "star-filled" : "star"} size="small" />
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={expanded()}>
|
||||
<button
|
||||
class={`model-selector-item-select-btn${showSelectBtn() ? "" : " model-selector-item-select-btn--hidden"}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
selectRow(row)
|
||||
}}
|
||||
>
|
||||
{language.t("dialog.model.select")}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
|
||||
+2
@@ -176,6 +176,8 @@ export const dict = {
|
||||
"model.tag.latest": "الأحدث",
|
||||
"model.group.recommended": "موصى به",
|
||||
"model.group.favorites": "المفضلة",
|
||||
"model.group.collapse": "طي {{group}}",
|
||||
"model.group.expand": "توسيع {{group}}",
|
||||
"model.favorite.add": "إضافة إلى المفضلة",
|
||||
"model.favorite.remove": "إزالة من المفضلة",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
|
||||
+2
@@ -177,6 +177,8 @@ export const dict = {
|
||||
"model.tag.latest": "Mais recente",
|
||||
"model.group.recommended": "Recomendado",
|
||||
"model.group.favorites": "Favoritos",
|
||||
"model.group.collapse": "Recolher {{group}}",
|
||||
"model.group.expand": "Expandir {{group}}",
|
||||
"model.favorite.add": "Adicionar aos favoritos",
|
||||
"model.favorite.remove": "Remover dos favoritos",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
|
||||
+2
@@ -178,6 +178,8 @@ export const dict = {
|
||||
"model.tag.latest": "Najnovije",
|
||||
"model.group.recommended": "Preporučeno",
|
||||
"model.group.favorites": "Favoriti",
|
||||
"model.group.collapse": "Sakrij {{group}}",
|
||||
"model.group.expand": "Prikaži {{group}}",
|
||||
"model.favorite.add": "Dodaj u favorite",
|
||||
"model.favorite.remove": "Ukloni iz favorita",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
|
||||
+2
@@ -177,6 +177,8 @@ export const dict = {
|
||||
"model.tag.latest": "Nyeste",
|
||||
"model.group.recommended": "Anbefalet",
|
||||
"model.group.favorites": "Favoritter",
|
||||
"model.group.collapse": "Skjul {{group}}",
|
||||
"model.group.expand": "Vis {{group}}",
|
||||
"model.favorite.add": "Føj til favoritter",
|
||||
"model.favorite.remove": "Fjern fra favoritter",
|
||||
|
||||
|
||||
+2
@@ -181,6 +181,8 @@ export const dict = {
|
||||
"model.tag.latest": "Neueste",
|
||||
"model.group.recommended": "Empfohlen",
|
||||
"model.group.favorites": "Favoriten",
|
||||
"model.group.collapse": "{{group}} einklappen",
|
||||
"model.group.expand": "{{group}} ausklappen",
|
||||
"model.favorite.add": "Zu Favoriten hinzufügen",
|
||||
"model.favorite.remove": "Aus Favoriten entfernen",
|
||||
|
||||
|
||||
@@ -178,6 +178,8 @@ export const dict = {
|
||||
"model.tag.latest": "Latest",
|
||||
"model.group.recommended": "Recommended",
|
||||
"model.group.favorites": "Favorites",
|
||||
"model.group.collapse": "Collapse {{group}}",
|
||||
"model.group.expand": "Expand {{group}}",
|
||||
"model.favorite.add": "Add to favorites",
|
||||
"model.favorite.remove": "Remove from favorites",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
|
||||
+2
@@ -178,6 +178,8 @@ export const dict = {
|
||||
"model.tag.latest": "Último",
|
||||
"model.group.recommended": "Recomendado",
|
||||
"model.group.favorites": "Favoritos",
|
||||
"model.group.collapse": "Contraer {{group}}",
|
||||
"model.group.expand": "Expandir {{group}}",
|
||||
"model.favorite.add": "Añadir a favoritos",
|
||||
"model.favorite.remove": "Eliminar de favoritos",
|
||||
|
||||
|
||||
+2
@@ -179,6 +179,8 @@ export const dict = {
|
||||
"model.tag.latest": "Dernier",
|
||||
"model.group.recommended": "Recommandé",
|
||||
"model.group.favorites": "Favoris",
|
||||
"model.group.collapse": "Réduire {{group}}",
|
||||
"model.group.expand": "Développer {{group}}",
|
||||
"model.favorite.add": "Ajouter aux favoris",
|
||||
"model.favorite.remove": "Retirer des favoris",
|
||||
|
||||
|
||||
+2
@@ -176,6 +176,8 @@ export const dict = {
|
||||
"model.tag.latest": "最新",
|
||||
"model.group.recommended": "推奨",
|
||||
"model.group.favorites": "お気に入り",
|
||||
"model.group.collapse": "{{group}} を折りたたむ",
|
||||
"model.group.expand": "{{group}} を展開",
|
||||
"model.favorite.add": "お気に入りに追加",
|
||||
"model.favorite.remove": "お気に入りから削除",
|
||||
|
||||
|
||||
+2
@@ -180,6 +180,8 @@ export const dict = {
|
||||
"model.tag.latest": "최신",
|
||||
"model.group.recommended": "추천",
|
||||
"model.group.favorites": "즐겨찾기",
|
||||
"model.group.collapse": "{{group}} 접기",
|
||||
"model.group.expand": "{{group}} 펼치기",
|
||||
"model.favorite.add": "즐겨찾기에 추가",
|
||||
"model.favorite.remove": "즐겨찾기에서 제거",
|
||||
|
||||
|
||||
+2
@@ -178,6 +178,8 @@ export const dict = {
|
||||
"model.tag.latest": "Nieuwste",
|
||||
"model.group.recommended": "Aanbevolen",
|
||||
"model.group.favorites": "Favorieten",
|
||||
"model.group.collapse": "{{group}} inklappen",
|
||||
"model.group.expand": "{{group}} uitklappen",
|
||||
"model.favorite.add": "Toevoegen aan favorieten",
|
||||
"model.favorite.remove": "Verwijderen uit favorieten",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
|
||||
+2
@@ -180,6 +180,8 @@ export const dict = {
|
||||
"model.tag.latest": "Nyeste",
|
||||
"model.group.recommended": "Anbefalt",
|
||||
"model.group.favorites": "Favoritter",
|
||||
"model.group.collapse": "Skjul {{group}}",
|
||||
"model.group.expand": "Vis {{group}}",
|
||||
"model.favorite.add": "Legg til i favoritter",
|
||||
"model.favorite.remove": "Fjern fra favoritter",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
|
||||
+2
@@ -177,6 +177,8 @@ export const dict = {
|
||||
"model.tag.latest": "Najnowszy",
|
||||
"model.group.recommended": "Zalecane",
|
||||
"model.group.favorites": "Ulubione",
|
||||
"model.group.collapse": "Zwiń {{group}}",
|
||||
"model.group.expand": "Rozwiń {{group}}",
|
||||
"model.favorite.add": "Dodaj do ulubionych",
|
||||
"model.favorite.remove": "Usuń z ulubionych",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
|
||||
+2
@@ -177,6 +177,8 @@ export const dict = {
|
||||
"model.tag.latest": "Последняя",
|
||||
"model.group.recommended": "Рекомендуемые",
|
||||
"model.group.favorites": "Избранное",
|
||||
"model.group.collapse": "Свернуть {{group}}",
|
||||
"model.group.expand": "Развернуть {{group}}",
|
||||
"model.favorite.add": "Добавить в избранное",
|
||||
"model.favorite.remove": "Удалить из избранного",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
|
||||
+2
@@ -177,6 +177,8 @@ export const dict = {
|
||||
"model.tag.latest": "ล่าสุด",
|
||||
"model.group.recommended": "แนะนำ",
|
||||
"model.group.favorites": "รายการโปรด",
|
||||
"model.group.collapse": "ย่อ {{group}}",
|
||||
"model.group.expand": "ขยาย {{group}}",
|
||||
"model.favorite.add": "เพิ่มในรายการโปรด",
|
||||
"model.favorite.remove": "ลบออกจากรายการโปรด",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
|
||||
+2
@@ -177,6 +177,8 @@ export const dict = {
|
||||
"model.tag.latest": "En yeni",
|
||||
"model.group.recommended": "Önerilen",
|
||||
"model.group.favorites": "Favoriler",
|
||||
"model.group.collapse": "{{group}} grubunu daralt",
|
||||
"model.group.expand": "{{group}} grubunu genişlet",
|
||||
"model.favorite.add": "Favorilere ekle",
|
||||
"model.favorite.remove": "Favorilerden çıkar",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
|
||||
+2
@@ -178,6 +178,8 @@ export const dict = {
|
||||
"model.tag.latest": "Остання",
|
||||
"model.group.recommended": "Рекомендовані",
|
||||
"model.group.favorites": "Обране",
|
||||
"model.group.collapse": "Згорнути {{group}}",
|
||||
"model.group.expand": "Розгорнути {{group}}",
|
||||
"model.favorite.add": "Додати до обраного",
|
||||
"model.favorite.remove": "Видалити з обраного",
|
||||
"model.provider.anthropic": "Anthropic",
|
||||
|
||||
+2
@@ -177,6 +177,8 @@ export const dict = {
|
||||
"model.tag.latest": "最新",
|
||||
"model.group.recommended": "推荐",
|
||||
"model.group.favorites": "收藏夹",
|
||||
"model.group.collapse": "折叠 {{group}}",
|
||||
"model.group.expand": "展开 {{group}}",
|
||||
"model.favorite.add": "添加到收藏夹",
|
||||
"model.favorite.remove": "从收藏夹中移除",
|
||||
|
||||
|
||||
+2
@@ -177,6 +177,8 @@ export const dict = {
|
||||
"model.tag.latest": "最新",
|
||||
"model.group.recommended": "推薦",
|
||||
"model.group.favorites": "我的最愛",
|
||||
"model.group.collapse": "折疊 {{group}}",
|
||||
"model.group.expand": "展開 {{group}}",
|
||||
"model.favorite.add": "加入我的最愛",
|
||||
"model.favorite.remove": "從我的最愛中移除",
|
||||
|
||||
|
||||
@@ -253,6 +253,10 @@
|
||||
}
|
||||
|
||||
.model-selector-group-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
padding: 4px 12px;
|
||||
font-size: var(--font-size-x-small, 11px);
|
||||
font-weight: var(--font-weight-medium, 500);
|
||||
@@ -260,6 +264,20 @@
|
||||
letter-spacing: var(--letter-spacing-normal, 0);
|
||||
color: var(--text-weak, var(--vscode-descriptionForeground));
|
||||
user-select: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.model-selector-group-label:hover {
|
||||
color: var(--text-base, var(--vscode-foreground));
|
||||
}
|
||||
|
||||
.model-selector-group-label:focus-visible {
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.model-selector-group-label:not(:first-child) {
|
||||
@@ -268,6 +286,15 @@
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.model-selector-group-chevron {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.12s ease;
|
||||
}
|
||||
|
||||
.model-selector-group-chevron--collapsed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.model-selector-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user