mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
42bb938c09
* tmp * tmp * tmp * tmp * tmp * feat(vscode): multi-provider selection with connect/disconnect/OAuth flows Reimplements the provider management UI in the VS Code extension settings: - Split Providers tab into Models (model selection) and Providers (connection management) - Kilo Gateway always shown at top with login state from profile data - Provider connect dialog with API key and OAuth (code + auto) flows - Custom provider dialog for OpenAI-compatible providers via base URL - Provider action request-response pairing via createProviderAction utility - Coalesced fetchAndSendProviders prevents request floods (single in-flight + one queued) - SSE event deduplication: server.instance.disposed filtered by workspace directory - Login guard prevents concurrent device auth flows - Model selection fallback chain: override > mode config > global config > recents > kilo-auto - Recent models persisted in extension globalState (last 5, deduplicated) - Shared validation in src/shared/ (provider-model.ts, custom-provider.ts) - 16 locale translations for all new UI strings - Unit tests for custom-provider validation, model selection, provider actions, visibility * Add tests * chore: update kilo-vscode visual regression baselines * fix(vscode): log swallowed auth.remove errors for configured providers When a configured provider has both a config entry and an auth store entry, and auth.remove fails transiently, the error was silently swallowed. Now logs a warning so the failure is visible in debug output. * fix(vscode): validate recentModels shape and enforce size limit on extension side Webview messages are an untrusted boundary. persistRecents and requestRecents now validate array shape (providerID/modelID must be strings) and enforce RECENT_LIMIT=5 on the extension side, not just in the webview. Malformed globalState entries are filtered out on read. * fix(vscode): normalize directory paths in server.instance.disposed filter Use path.resolve() when comparing the event directory against the workspace directory. Prevents false mismatches from trailing slashes or case differences on case-insensitive filesystems. * fix(vscode): remove duplicate requestRecents case branches (dead code) Three consecutive case "requestRecents" blocks existed from iterative refinement. Only the first executes in a switch. Removed the two dead duplicates, keeping the validated version using validateRecents(). * fix(vscode): don't navigate to profile after login Login can now be triggered from the settings tab (Kilo Gateway sign-in button). The forced navigation to the profile view after login left the user stuck on the profile tab with no way back to settings without closing and reopening. The profile data push is sufficient — the settings Kilo row updates reactively via server.profileData(). * fix(vscode): mask API key input in custom provider dialog Add type="password" to the API key TextField so it renders as dots instead of plain text. * refactor(vscode): extract inline styles from ProviderConnectDialog to CSS classes Move repeated inline styles to chat.css classes: - .provider-connect-body (text body styling) - .provider-connect-code-label (confirmation code label) - .provider-connect-code (monospace code block) - .provider-connect-status (spinner + status text row) * fix: revert unrelated opencode/package.json changes * chore: update kilo-vscode visual regression baselines * fix(vscode): send rollback configUpdated on failed config save When global.config.update fails, send configUpdated with the last known good config so the webview clears its saving flag and reverts optimistic state. Without this, a failed save leaves saving=true forever, causing subsequent configLoaded messages to be ignored. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
107 lines
3.5 KiB
TypeScript
107 lines
3.5 KiB
TypeScript
import { describe, expect, it } from "bun:test"
|
|
import { resolveModelSelection } from "../../webview-ui/src/context/model-selection"
|
|
import { KILO_AUTO, parseModelString } from "../../src/shared/provider-model"
|
|
import type { Provider } from "../../webview-ui/src/types/messages"
|
|
|
|
function makeProvider(id: string, name: string, modelIds: string[]): Provider {
|
|
const models: Provider["models"] = {}
|
|
for (const modelID of modelIds) {
|
|
models[modelID] = { id: modelID, name: modelID }
|
|
}
|
|
return { id, name, models }
|
|
}
|
|
|
|
const providers = {
|
|
kilo: makeProvider("kilo", "Kilo Gateway", ["kilo-auto/free"]),
|
|
anthropic: makeProvider("anthropic", "Anthropic", ["claude-sonnet-4"]),
|
|
openai: makeProvider("openai", "OpenAI", ["gpt-4.1"]),
|
|
}
|
|
|
|
describe("parseModelString", () => {
|
|
it("parses provider/model pairs", () => {
|
|
expect(parseModelString("anthropic/claude-sonnet-4")).toEqual({
|
|
providerID: "anthropic",
|
|
modelID: "claude-sonnet-4",
|
|
})
|
|
})
|
|
|
|
it("keeps slashes inside kilo model ids", () => {
|
|
expect(parseModelString("kilo/kilo-auto/free")).toEqual({
|
|
providerID: "kilo",
|
|
modelID: "kilo-auto/free",
|
|
})
|
|
})
|
|
|
|
it("returns null for invalid values", () => {
|
|
expect(parseModelString(undefined)).toBeNull()
|
|
expect(parseModelString("claude-sonnet-4")).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe("resolveModelSelection", () => {
|
|
it("prefers a valid override", () => {
|
|
const result = resolveModelSelection({
|
|
providers,
|
|
connected: ["anthropic", "openai"],
|
|
override: { providerID: "openai", modelID: "gpt-4.1" },
|
|
mode: { providerID: "anthropic", modelID: "claude-sonnet-4" },
|
|
fallback: KILO_AUTO,
|
|
})
|
|
expect(result).toEqual({ providerID: "openai", modelID: "gpt-4.1" })
|
|
})
|
|
|
|
it("falls back from an invalid override to the mode model", () => {
|
|
const result = resolveModelSelection({
|
|
providers,
|
|
connected: ["anthropic"],
|
|
override: { providerID: "openai", modelID: "gpt-4.1" },
|
|
mode: { providerID: "anthropic", modelID: "claude-sonnet-4" },
|
|
fallback: KILO_AUTO,
|
|
})
|
|
expect(result).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4" })
|
|
})
|
|
|
|
it("falls back from invalid config to the first valid recent model", () => {
|
|
const result = resolveModelSelection({
|
|
providers,
|
|
connected: ["openai"],
|
|
mode: { providerID: "anthropic", modelID: "claude-sonnet-4" },
|
|
recent: [
|
|
{ providerID: "anthropic", modelID: "claude-sonnet-4" },
|
|
{ providerID: "openai", modelID: "gpt-4.1" },
|
|
],
|
|
fallback: KILO_AUTO,
|
|
})
|
|
expect(result).toEqual({ providerID: "openai", modelID: "gpt-4.1" })
|
|
})
|
|
|
|
it("uses kilo auto as the explicit final fallback", () => {
|
|
const result = resolveModelSelection({
|
|
providers,
|
|
connected: [],
|
|
fallback: KILO_AUTO,
|
|
})
|
|
expect(result).toEqual(KILO_AUTO)
|
|
})
|
|
|
|
it("keeps the explicit fallback even when kilo is missing from the loaded catalog", () => {
|
|
const result = resolveModelSelection({
|
|
providers: { openai: providers.openai },
|
|
connected: [],
|
|
fallback: KILO_AUTO,
|
|
})
|
|
expect(result).toEqual(KILO_AUTO)
|
|
})
|
|
|
|
it("keeps the raw preference order before providers load", () => {
|
|
const result = resolveModelSelection({
|
|
providers: {},
|
|
connected: [],
|
|
override: { providerID: "openai", modelID: "gpt-4.1" },
|
|
mode: { providerID: "anthropic", modelID: "claude-sonnet-4" },
|
|
fallback: KILO_AUTO,
|
|
})
|
|
expect(result).toEqual({ providerID: "openai", modelID: "gpt-4.1" })
|
|
})
|
|
})
|