diff --git a/.changeset/quiet-pandas-hide.md b/.changeset/quiet-pandas-hide.md new file mode 100644 index 0000000000..9853974187 --- /dev/null +++ b/.changeset/quiet-pandas-hide.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Restore disabled provider management in the VS Code extension provider settings. diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/providers-configure-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/providers-configure-chromium-linux.png index df62efbf2c..996569f48f 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/providers-configure-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/providers-configure-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c67c86c68c4a07514db50c964668753c0a85ec410e55208b6e094f711c0bceb5 -size 20317 +oid sha256:9c87ac5179843ad5dadf73023f87d1f0b0589a81420e9ef406534a335598a0a2 +size 28082 diff --git a/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts b/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts index 337dd77a14..a045e2e1f7 100644 --- a/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts +++ b/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts @@ -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 diff --git a/packages/kilo-vscode/tests/unit/provider-actions-save.test.ts b/packages/kilo-vscode/tests/unit/provider-actions-save.test.ts index 5a52a31510..2e04caa3ad 100644 --- a/packages/kilo-vscode/tests/unit/provider-actions-save.test.ts +++ b/packages/kilo-vscode/tests/unit/provider-actions-save.test.ts @@ -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", () => { diff --git a/packages/kilo-vscode/tests/unit/provider-visibility.test.ts b/packages/kilo-vscode/tests/unit/provider-visibility.test.ts index 5b71ad7ef9..3efd293f64 100644 --- a/packages/kilo-vscode/tests/unit/provider-visibility.test.ts +++ b/packages/kilo-vscode/tests/unit/provider-visibility.test.ts @@ -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") + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx index 217f845309..f43f8f7ea5 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx @@ -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() 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 (
- {/* Kilo Gateway — always at the top, not editable */} - -
- - - Kilo Gateway - - server.startLogin()}> - {language.t("common.signIn")} - - } + + {/* Kilo Gateway — always at the top, not editable */} + +
- {language.t("settings.providers.tag.gateway")} - -
-
+ + + Kilo Gateway + + server.startLogin()}> + {language.t("common.signIn")} + + } + > + {language.t("settings.providers.tag.gateway")} + +
+
+ {/* Connected providers (excluding Kilo) */}

@@ -342,6 +368,92 @@ const ProvidersTab: Component = () => { {language.t("dialog.provider.viewAll")}

+ + {/* Disabled providers */} +

{language.t("settings.providers.disabled")}

+ +
+ {language.t("settings.providers.disabled.description")} +
+
0 ? "1px solid var(--border-weak-base)" : "none", + }} + > +
+