mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
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:
+2
-2
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user