fix(vscode): restore disabled provider management (#9551)

* fix(vscode): restore disabled provider management

* fix(vscode): allow disabling Kilo Gateway

* fixup! fix(vscode): allow disabling Kilo Gateway

* fixup! fixup! fix(vscode): allow disabling Kilo Gateway

* chore: update kilo-vscode visual regression baselines

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Marius
2026-04-27 12:22:43 +02:00
committed by GitHub
parent fc9cf245a5
commit b344ac97c6
7 changed files with 277 additions and 34 deletions
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c67c86c68c4a07514db50c964668753c0a85ec410e55208b6e094f711c0bceb5
size 20317
oid sha256:9c87ac5179843ad5dadf73023f87d1f0b0589a81420e9ef406534a335598a0a2
size 28082
@@ -28,6 +28,18 @@ function args(form: FormState) {
}
describe("validateCustomProvider variant name validation", () => {
it("allows reconnecting a disabled provider id", () => {
const form = base()
const out = validateCustomProvider({
...args(form),
disabledProviders: ["my-provider"],
existingProviderIDs: new Set(["my-provider"]),
})
expect(out.result?.providerID).toBe("my-provider")
expect(out.errors.providerID).toBeUndefined()
})
it("allows submit when reasoning is enabled with no variants", () => {
const form = base()
form.models[0].reasoning = true
@@ -229,6 +229,50 @@ describe("saveCustomProvider", () => {
.models
expect(Object.values(models).every((v) => v !== null)).toBe(true)
})
it("removes saved custom providers from disabled_providers when reconnecting", async () => {
const { ctx, calls, setCachedConfig } = createCtx({ disabled_providers: ["myprovider", "openai"] })
await saveCustomProvider(ctx, "req", "myprovider", createProvider(), undefined, false, null, setCachedConfig)
expect(calls.config).toHaveLength(1)
expect(calls.config[0].config.disabled_providers).toEqual(["openai"])
})
})
describe("disconnectProvider", () => {
it("adds configured providers to disabled_providers without deleting their config", async () => {
const existing = {
disabled_providers: ["openai"],
provider: {
myprovider: createProvider(),
},
}
const { ctx, calls, setCachedConfig } = createCtx(existing)
await disconnectProvider(ctx, "req", "myprovider", null, setCachedConfig)
expect(calls.config).toHaveLength(1)
expect(calls.config[0].config).toEqual({ disabled_providers: ["openai", "myprovider"] })
expect(calls.remove).toEqual([{ providerID: "myprovider" }])
expect(calls.refresh).toBe(1)
expect(calls.posts).toContainEqual({ type: "providerDisconnected", requestId: "req", providerID: "myprovider" })
})
it("does not duplicate configured providers already disabled", async () => {
const existing = {
disabled_providers: ["myprovider"],
provider: {
myprovider: createProvider(),
},
}
const { ctx, calls, setCachedConfig } = createCtx(existing)
await disconnectProvider(ctx, "req", "myprovider", null, setCachedConfig)
expect(calls.config).toHaveLength(0)
expect(calls.refresh).toBe(1)
})
})
describe("fetchProviderData", () => {
@@ -1,6 +1,10 @@
import { describe, expect, it } from "bun:test"
import { visibleConnectedIds } from "../../webview-ui/src/components/settings/provider-visibility"
import {
disabledProviderOptions,
providersWithKiloFallback,
visibleConnectedIds,
} from "../../webview-ui/src/components/settings/provider-visibility"
describe("visibleConnectedIds", () => {
it("hides Kilo from the connected list when auth is missing", () => {
@@ -21,3 +25,55 @@ describe("visibleConnectedIds", () => {
expect(ids).toEqual(["anthropic"])
})
})
describe("disabledProviderOptions", () => {
it("includes Kilo and excludes already disabled providers", () => {
const options = disabledProviderOptions(
{
kilo: { id: "kilo", name: "Kilo Gateway", env: [], models: {} },
openai: { id: "openai", name: "OpenAI", env: [], models: {} },
anthropic: { id: "anthropic", name: "Anthropic", env: [], models: {} },
},
["openai"],
)
expect(options).toEqual([
{ value: "anthropic", label: "Anthropic" },
{ value: "kilo", label: "Kilo Gateway" },
])
})
it("sorts options by provider name", () => {
const options = disabledProviderOptions(
{
zed: { id: "zed", name: "Zed", env: [], models: {} },
alpha: { id: "alpha", name: "Alpha", env: [], models: {} },
},
[],
)
expect(options).toEqual([
{ value: "alpha", label: "Alpha" },
{ value: "zed", label: "Zed" },
])
})
})
describe("providersWithKiloFallback", () => {
it("adds Kilo when backend providers omit it", () => {
const providers = providersWithKiloFallback({
anthropic: { id: "anthropic", name: "Anthropic", env: [], models: {} },
})
expect(providers.kilo?.name).toBe("Kilo Gateway")
expect(providers.anthropic?.name).toBe("Anthropic")
})
it("keeps the backend Kilo provider when present", () => {
const providers = providersWithKiloFallback({
kilo: { id: "kilo", name: "Custom Kilo Name", env: [], models: {} },
})
expect(providers.kilo?.name).toBe("Custom Kilo Name")
})
})
@@ -1,11 +1,11 @@
import { Button } from "@kilocode/kilo-ui/button"
import { Card } from "@kilocode/kilo-ui/card"
import { useDialog } from "@kilocode/kilo-ui/context/dialog"
import { Icon } from "@kilocode/kilo-ui/icon"
import { ProviderIcon } from "@kilocode/kilo-ui/provider-icon"
import { Select } from "@kilocode/kilo-ui/select"
import { Tag } from "@kilocode/kilo-ui/tag"
import { showToast } from "@kilocode/kilo-ui/toast"
import { Component, For, Show, createMemo, onCleanup } from "solid-js"
import { Component, For, Show, createMemo, createSignal, onCleanup } from "solid-js"
import { useConfig } from "../../context/config"
import { useLanguage } from "../../context/language"
import { useProvider } from "../../context/provider"
@@ -16,20 +16,22 @@ import CustomProviderDialog from "./CustomProviderDialog"
import ProviderConnectDialog from "./ProviderConnectDialog"
import ProviderSelectDialog from "./ProviderSelectDialog"
import { CUSTOM_PROVIDER_ID, isPopularProvider, providerIcon, providerNoteKey, sortProviders } from "./provider-catalog"
import { visibleConnectedIds } from "./provider-visibility"
import { disabledProviderOptions, providersWithKiloFallback, visibleConnectedIds } from "./provider-visibility"
import { KILO_PROVIDER_ID, CUSTOM_PROVIDER_PACKAGE } from "../../../../src/shared/provider-model"
import { createProviderAction } from "../../utils/provider-action"
type ProviderSource = "env" | "api" | "config" | "custom"
type ProviderOption = { value: string; label: string }
const ProvidersTab: Component = () => {
const dialog = useDialog()
const { config } = useConfig()
const { config, updateConfig } = useConfig()
const provider = useProvider()
const language = useLanguage()
const server = useServer()
const vscode = useVSCode()
const action = createProviderAction(vscode)
const [disabled, setDisabled] = createSignal<ProviderOption | undefined>()
onCleanup(action.dispose)
@@ -59,6 +61,11 @@ const ProvidersTab: Component = () => {
)
})
const disabledProviders = createMemo(() => config().disabled_providers ?? [])
const disabledIds = createMemo(() => new Set(disabledProviders()))
const providers = createMemo(() => providersWithKiloFallback(provider.providers()))
const disabledOptions = createMemo(() => disabledProviderOptions(providers(), disabledProviders()))
function source(item: Provider): ProviderSource | undefined {
if (!("source" in item)) return
const value = (item as Provider & { source?: string }).source
@@ -114,6 +121,23 @@ const ProvidersTab: Component = () => {
)
}
function disableProvider(providerID: string) {
const current = disabledProviders()
if (!providerID || current.includes(providerID)) return
updateConfig({ disabled_providers: [...current, providerID] })
}
function enableProvider(index: number) {
const next = [...disabledProviders()]
next.splice(index, 1)
updateConfig({ disabled_providers: next })
}
function disabledName(id: string) {
const item = providers()[id]
return item?.name ?? id
}
function connectProvider(item: Provider) {
if (item.id === KILO_PROVIDER_ID) {
server.startLogin()
@@ -134,33 +158,35 @@ const ProvidersTab: Component = () => {
return (
<div>
{/* Kilo Gateway — always at the top, not editable */}
<Card>
<div
style={{
display: "flex",
"align-items": "center",
gap: "12px",
"min-height": "56px",
padding: "12px 0",
}}
>
<ProviderIcon id="synthetic" width={20} height={20} />
<span style={{ "font-size": "14px", "font-weight": "500", color: "var(--vscode-foreground)" }}>
Kilo Gateway
</span>
<Show
when={kiloLoggedIn()}
fallback={
<Button size="small" variant="secondary" onClick={() => server.startLogin()}>
{language.t("common.signIn")}
</Button>
}
<Show when={!disabledIds().has(KILO_PROVIDER_ID)}>
{/* Kilo Gateway — always at the top, not editable */}
<Card>
<div
style={{
display: "flex",
"align-items": "center",
gap: "12px",
"min-height": "56px",
padding: "12px 0",
}}
>
<Tag>{language.t("settings.providers.tag.gateway")}</Tag>
</Show>
</div>
</Card>
<ProviderIcon id="synthetic" width={20} height={20} />
<span style={{ "font-size": "14px", "font-weight": "500", color: "var(--vscode-foreground)" }}>
Kilo Gateway
</span>
<Show
when={kiloLoggedIn()}
fallback={
<Button size="small" variant="secondary" onClick={() => server.startLogin()}>
{language.t("common.signIn")}
</Button>
}
>
<Tag>{language.t("settings.providers.tag.gateway")}</Tag>
</Show>
</div>
</Card>
</Show>
{/* Connected providers (excluding Kilo) */}
<h4 style={{ "margin-top": "16px", "margin-bottom": "8px" }}>
@@ -342,6 +368,92 @@ const ProvidersTab: Component = () => {
{language.t("dialog.provider.viewAll")}
</Button>
</div>
{/* Disabled providers */}
<h4 style={{ "margin-top": "24px", "margin-bottom": "8px" }}>{language.t("settings.providers.disabled")}</h4>
<Card>
<div
style={{
"font-size": "12px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
"padding-bottom": "8px",
"border-bottom": "1px solid var(--border-weak-base)",
}}
>
{language.t("settings.providers.disabled.description")}
</div>
<div
style={{
display: "flex",
gap: "8px",
"align-items": "center",
padding: "8px 0",
"border-bottom": disabledProviders().length > 0 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<div style={{ flex: 1 }}>
<Select
options={disabledOptions()}
current={disabled()}
value={(item) => item.value}
label={(item) => item.label}
onSelect={(item) => setDisabled(item)}
variant="secondary"
triggerVariant="settings"
placeholder={language.t("settings.providers.select.placeholder")}
/>
</div>
<Button
variant="secondary"
onClick={() => {
const item = disabled()
if (!item) return
disableProvider(item.value)
setDisabled(undefined)
}}
disabled={!disabled()}
>
{language.t("common.add")}
</Button>
</div>
<For each={disabledProviders()}>
{(id, index) => (
<div
style={{
display: "flex",
"flex-wrap": "wrap",
"align-items": "center",
"justify-content": "space-between",
gap: "16px",
"min-height": "56px",
padding: "12px 0",
"border-bottom":
index() < disabledProviders().length - 1 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<div style={{ display: "flex", "align-items": "center", gap: "12px", "min-width": 0 }}>
<ProviderIcon id={providerIcon(id)} width={20} height={20} />
<span
style={{
"font-size": "14px",
"font-weight": "500",
color: "var(--vscode-foreground)",
overflow: "hidden",
"text-overflow": "ellipsis",
"white-space": "nowrap",
}}
>
{disabledName(id)}
</span>
<Tag>{language.t("settings.providers.disabled")}</Tag>
</div>
<Button size="large" variant="ghost" onClick={() => enableProvider(index())}>
{language.t("common.delete")}
</Button>
</div>
)}
</For>
</Card>
</div>
)
}
@@ -1,6 +1,20 @@
import type { ProviderAuthState } from "../../types/messages"
import { KILO_PROVIDER_ID } from "../../../../src/shared/provider-model"
import type { Provider } from "../../types/messages"
import { KILO_PROVIDER_ID, createKiloFallbackProvider } from "../../../../src/shared/provider-model"
export function visibleConnectedIds(connected: string[], authStates: Record<string, ProviderAuthState>) {
return connected.filter((id) => id !== KILO_PROVIDER_ID || authStates[KILO_PROVIDER_ID] !== undefined)
}
export function disabledProviderOptions(providers: Record<string, Provider>, disabled: string[]) {
const current = new Set(disabled)
return Object.values(providers)
.filter((item) => !current.has(item.id))
.map((item) => ({ value: item.id, label: item.name }))
.sort((a, b) => a.label.localeCompare(b.label))
}
export function providersWithKiloFallback(providers: Record<string, Provider>): Record<string, Provider> {
if (providers[KILO_PROVIDER_ID]) return providers
return { [KILO_PROVIDER_ID]: createKiloFallbackProvider(), ...providers }
}