diff --git a/packages/kilo-vscode/eslint.config.mjs b/packages/kilo-vscode/eslint.config.mjs
index 348dd756a7..f82b5a9d93 100644
--- a/packages/kilo-vscode/eslint.config.mjs
+++ b/packages/kilo-vscode/eslint.config.mjs
@@ -31,5 +31,11 @@ export default [
"max-lines": ["error", 3000],
},
},
+ {
+ files: ["src/KiloProvider.ts"],
+ rules: {
+ "max-lines": ["error", 3200],
+ },
+ },
eslintConfigPrettier,
]
diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts
index 72faba0af6..d2f02adb14 100644
--- a/packages/kilo-vscode/src/KiloProvider.ts
+++ b/packages/kilo-vscode/src/KiloProvider.ts
@@ -39,6 +39,18 @@ import { MarketplaceService } from "./services/marketplace"
import { resolveProjectDirectory } from "./project-directory"
import { getBusySessionCount, seedSessionStatuses } from "./session-status"
+import {
+ buildActionContext,
+ computeDefaultSelection,
+ fetchProviderData,
+ validateRecents,
+ connectProvider as connectProviderAction,
+ authorizeProviderOAuth as authorizeOAuthAction,
+ completeProviderOAuth as completeOAuthAction,
+ disconnectProvider as disconnectProviderAction,
+ saveCustomProvider as saveCustomProviderAction,
+} from "./provider-actions"
+
type KiloProviderOptions = {
projectDirectory?: string | null
slimEditMetadata?: boolean
@@ -56,6 +68,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
vscode.extensions.getExtension("kilocode.kilo-code")?.packageJSON?.version ?? "unknown"
/** Cached providersLoaded payload so requestProviders can be served before client is ready */
private cachedProvidersMessage: unknown = null
+ /** Coalesce provider refreshes — at most one follow-up rerun when a request lands mid-flight. */
+ private providersRefresh: Promise | null = null
+ private providersQueued = false
+ private providersGeneration = 0
/** Cached agentsLoaded payload so requestAgents can be served before client is ready */
private cachedAgentsMessage: unknown = null
/** Cached skillsLoaded payload so requestSkills can be served before client is ready */
@@ -548,6 +564,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "requestProviders":
this.fetchAndSendProviders().catch((e) => console.error("[Kilo New] fetchAndSendProviders failed:", e))
break
+ case "connectProvider":
+ case "authorizeProviderOAuth":
+ case "completeProviderOAuth":
+ case "disconnectProvider":
+ case "saveCustomProvider":
+ await this.handleProviderAction(message)
+ break
case "compact":
await this.handleCompact(message.sessionID, message.providerID, message.modelID)
break
@@ -718,6 +741,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.postMessage({ type: "variantsLoaded", variants })
break
}
+ case "persistRecents":
+ await this.extensionContext?.globalState.update("recentModels", validateRecents(message.recents))
+ break
+ case "requestRecents": {
+ const recents = validateRecents(this.extensionContext?.globalState.get("recentModels"))
+ this.postMessage({ type: "recentsLoaded", recents })
+ break
+ }
// legacy-migration start
case "requestLegacyMigrationData":
void this.handleRequestLegacyMigrationData()
@@ -1225,45 +1256,106 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}
- /**
- * Fetch providers from the backend and send to webview.
- *
- * The backend `/provider` endpoint returns `all` as an array-like object with
- * numeric keys ("0", "1", …). The webview and sendMessage both need providers
- * keyed by their real `provider.id` (e.g. "anthropic", "openai"). We re-key
- * the map here so the rest of the code can use provider.id everywhere.
- */
+ /** Fetch providers and send to webview. Coalesced: at most one in-flight + one queued. */
private async fetchAndSendProviders(): Promise {
- if (!this.client) {
- // client not ready — serve from cache if available
- if (this.cachedProvidersMessage) {
- this.postMessage(this.cachedProvidersMessage)
- }
+ const next = ++this.providersGeneration
+ if (this.providersRefresh) {
+ this.providersQueued = true
+ await this.providersRefresh
return
}
-
- try {
- const workspaceDir = this.getWorkspaceDirectory()
- const { data: response } = await this.client.provider.list({ directory: workspaceDir }, { throwOnError: true })
-
- const normalized = indexProvidersById(response.all)
-
- const config = vscode.workspace.getConfiguration("kilo-code.new.model")
- const providerID = config.get("providerID", "kilo")
- const modelID = config.get("modelID", "kilo-auto/free")
-
- const message = {
- type: "providersLoaded",
- providers: normalized,
- connected: response.connected,
- defaults: response.default,
- defaultSelection: { providerID, modelID },
+ const task = (async () => {
+ let generation = next
+ while (true) {
+ this.providersQueued = false
+ const client = this.client
+ if (!client) {
+ if (this.cachedProvidersMessage && generation === this.providersGeneration)
+ this.postMessage(this.cachedProvidersMessage)
+ return
+ }
+ try {
+ const { response, authMethods, authStates } = await fetchProviderData(client, this.getWorkspaceDirectory())
+ if (generation !== this.providersGeneration || client !== this.client) {
+ if (!this.providersQueued) return
+ generation = this.providersGeneration
+ continue
+ }
+ const settings = vscode.workspace.getConfiguration("kilo-code.new.model")
+ const message = {
+ type: "providersLoaded",
+ providers: indexProvidersById(response.all),
+ connected: response.connected,
+ defaults: response.default,
+ defaultSelection: computeDefaultSelection(
+ this.cachedConfigMessage as { config?: { model?: string } } | null,
+ settings.get("providerID", ""),
+ settings.get("modelID", ""),
+ ),
+ authMethods,
+ authStates,
+ }
+ this.cachedProvidersMessage = message
+ this.postMessage(message)
+ } catch (error) {
+ if (generation !== this.providersGeneration) {
+ if (!this.providersQueued) return
+ generation = this.providersGeneration
+ continue
+ }
+ console.error("[Kilo New] KiloProvider: Failed to fetch providers:", error)
+ }
+ if (!this.providersQueued) return
+ generation = this.providersGeneration
}
- this.cachedProvidersMessage = message
- this.postMessage(message)
- } catch (error) {
- console.error("[Kilo New] KiloProvider: Failed to fetch providers:", error)
+ })()
+ const done = task.finally(() => {
+ if (this.providersRefresh === done) this.providersRefresh = null
+ })
+ this.providersRefresh = done
+ await done
+ }
+
+ private async handleProviderAction(msg: Record): Promise {
+ const rid = typeof msg.requestId === "string" ? msg.requestId : ""
+ const pid = typeof msg.providerID === "string" ? msg.providerID : ""
+ if (!rid || !pid) return
+ if (!this.client) {
+ const action =
+ msg.type === "disconnectProvider"
+ ? "disconnect"
+ : msg.type === "authorizeProviderOAuth"
+ ? "authorize"
+ : "connect"
+ this.postMessage({
+ type: "providerActionError",
+ requestId: rid,
+ providerID: pid,
+ action,
+ message: "Not connected to CLI backend",
+ })
+ return
}
+ const ctx = buildActionContext(
+ this.client,
+ (m) => this.postMessage(m),
+ getErrorMessage,
+ this.getWorkspaceDirectory(),
+ () => this.fetchAndSendProviders(),
+ )
+ const set = (m: unknown) => {
+ this.cachedConfigMessage = m
+ }
+ const method = typeof msg.method === "number" ? msg.method : 0
+ const key = typeof msg.apiKey === "string" ? msg.apiKey : undefined
+ const code = typeof msg.code === "string" ? msg.code : undefined
+ const config = msg.config && typeof msg.config === "object" ? (msg.config as Record) : undefined
+ if (msg.type === "connectProvider" && key) return connectProviderAction(ctx, rid, pid, key)
+ if (msg.type === "authorizeProviderOAuth") return authorizeOAuthAction(ctx, rid, pid, method)
+ if (msg.type === "completeProviderOAuth") return completeOAuthAction(ctx, rid, pid, method, code)
+ if (msg.type === "disconnectProvider") return disconnectProviderAction(ctx, rid, pid, this.cachedConfigMessage, set)
+ if (msg.type === "saveCustomProvider" && config)
+ return saveCustomProviderAction(ctx, rid, pid, config, key, this.cachedConfigMessage, set)
}
/**
@@ -1884,6 +1976,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
return
}
+ const refreshProviders =
+ partial.provider !== undefined ||
+ partial.disabled_providers !== undefined ||
+ partial.enabled_providers !== undefined
+
// Belt-and-suspenders guard: prevent fetchAndSendConfig from sending a
// stale configLoaded while this write is in flight (the SSE-triggered reload
// races with the async config.update() write on the CLI backend).
@@ -1900,12 +1997,21 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.cachedConfigMessage = { type: "configLoaded", config: merged }
this.postMessage({ type: "configUpdated", config: merged })
+
+ if (refreshProviders) {
+ await this.fetchAndSendProviders()
+ }
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to update config:", error)
this.postMessage({
type: "error",
message: getErrorMessage(error) || "Failed to update config",
})
+ // Send configUpdated with the last known good config so the webview
+ // clears its saving flag and reverts optimistic state.
+ if (this.cachedConfigMessage) {
+ this.postMessage({ type: "configUpdated", config: (this.cachedConfigMessage as { config: unknown }).config })
+ }
} finally {
this.pending--
}
@@ -2340,11 +2446,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
const { data: profileData } = await this.client.kilo.profile(undefined, { throwOnError: true })
this.postMessage({ type: "profileData", data: profileData })
this.postMessage({ type: "deviceAuthComplete" })
-
- // Step 5: If user has organizations, navigate to profile view so they can pick one
- if (profileData?.profile?.organizations && profileData.profile.organizations.length > 0) {
- this.postMessage({ type: "navigate", view: "profile" })
- }
} catch (error) {
if (attempt !== this.loginAttempt) {
return
@@ -2481,6 +2582,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
await this.client.global
.dispose()
.catch((e: unknown) => console.warn("[Kilo New] KiloProvider: global.dispose() after logout failed:", e))
+
+ await this.fetchAndSendProviders()
} catch (error) {
console.error("[Kilo New] KiloProvider: ❌ Logout failed:", error)
this.postMessage({
@@ -2566,20 +2669,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
})
}
- /**
- * Extract sessionID from an SSE event, if applicable.
- * Returns undefined for global events (server.connected, server.heartbeat).
- */
- private extractSessionID(event: Event): string | undefined {
- return this.connectionService.resolveEventSessionId(event)
- }
-
- /**
- * Re-fetch all server-side state after an auth change (login/logout/org switch).
- * After instance.dispose() clears the server cache, the next request to each
- * endpoint will re-initialize with the current auth state.
- * This mirrors the TUI's sync.bootstrap() pattern.
- */
+ /** Re-fetch all server-side state after an auth change. */
private async reloadAfterAuthChange(): Promise {
await Promise.all([
this.fetchAndSendProviders(),
@@ -2615,7 +2705,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
// Extract sessionID from the event
- const sessionID = this.extractSessionID(event)
+ const sessionID = this.connectionService.resolveEventSessionId(event)
// Events without sessionID (server.connected, server.heartbeat) → always forward
// Events with sessionID → only forward if this webview tracks that session
@@ -2628,7 +2718,15 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
// Refresh provider and agent lists when the server signals a state disposal
- if (event.type === "server.instance.disposed" || event.type === "global.disposed") {
+ if (event.type === "global.disposed") {
+ void this.reloadAfterAuthChange()
+ return
+ }
+
+ if (event.type === "server.instance.disposed") {
+ const props = event.properties as Record | null
+ const dir = typeof props?.directory === "string" ? props.directory : undefined
+ if (dir && path.resolve(dir) !== path.resolve(this.getWorkspaceDirectory())) return
void this.reloadAfterAuthChange()
return
}
diff --git a/packages/kilo-vscode/src/kilo-provider-utils.ts b/packages/kilo-vscode/src/kilo-provider-utils.ts
index 118b793a52..6245a24486 100644
--- a/packages/kilo-vscode/src/kilo-provider-utils.ts
+++ b/packages/kilo-vscode/src/kilo-provider-utils.ts
@@ -21,12 +21,25 @@ export function getErrorMessage(error: unknown): string {
const obj = error as Record
// Direct .message field
if (typeof obj.message === "string") return obj.message
- // Direct .error field
+ // Direct .error field (string)
if (typeof obj.error === "string") return obj.error
+ // SDK throwOnError shape: { error: { message: "..." } } or { error: { ... } }
+ if (obj.error && typeof obj.error === "object") {
+ const nested = obj.error as Record
+ if (typeof nested.message === "string") return nested.message
+ }
// NotFoundError shape: { data: { message: "..." } }
if (obj.data && typeof obj.data === "object") {
const data = obj.data as Record
if (typeof data.message === "string") return data.message
+ // Hono validator shape: { data: ..., error: [...], success: false }
+ if (Array.isArray(data.error) && data.error.length > 0) {
+ const first = data.error[0]
+ if (typeof first === "string") return first
+ if (first && typeof first === "object" && typeof (first as Record).message === "string") {
+ return (first as Record).message as string
+ }
+ }
}
// BadRequestError shape: { errors: [{ message: "..." }] }
if (Array.isArray(obj.errors) && obj.errors.length > 0) {
@@ -34,6 +47,13 @@ export function getErrorMessage(error: unknown): string {
if (typeof first === "string") return first
if (first && typeof first.message === "string") return first.message
}
+ // Last resort: try JSON.stringify for debuggability
+ try {
+ const json = JSON.stringify(error)
+ if (json !== "{}" && json.length < 500) return json
+ } catch (err) {
+ console.warn("[Kilo New] getErrorMessage: JSON.stringify failed", err)
+ }
}
return String(error)
}
diff --git a/packages/kilo-vscode/src/provider-actions.ts b/packages/kilo-vscode/src/provider-actions.ts
new file mode 100644
index 0000000000..44524fe9b0
--- /dev/null
+++ b/packages/kilo-vscode/src/provider-actions.ts
@@ -0,0 +1,301 @@
+/**
+ * Provider action handlers extracted from KiloProvider to stay under max-lines.
+ * These are pure async functions that operate on the SDK client — no vscode dependency.
+ */
+import type { KiloClient } from "@kilocode/sdk/v2"
+import { validateProviderID as validateProviderIDShared } from "./shared/custom-provider"
+import { sanitizeCustomProviderConfig } from "./shared/custom-provider"
+import { KILO_AUTO, parseModelString } from "./shared/provider-model"
+
+/**
+ * Compute the default model selection from CLI config, VS Code settings, or hardcoded fallback.
+ * Pure function — takes cachedConfig and vscode settings as parameters.
+ */
+type AuthState = "api" | "oauth" | "wellknown"
+
+/** Fetch auth methods alongside the provider list. Auth states default to empty (endpoint not yet available). */
+export async function fetchProviderData(client: KiloClient, dir: string) {
+ const authRequest =
+ typeof client.provider.auth === "function"
+ ? client.provider
+ .auth({ directory: dir }, { throwOnError: true })
+ .then((r) => r.data ?? {})
+ .catch(() => ({}))
+ : Promise.resolve({})
+
+ const [{ data: response }, authMethods] = await Promise.all([
+ client.provider.list({ directory: dir }, { throwOnError: true }),
+ authRequest,
+ ])
+ const authStates: Record = {}
+ return { response, authMethods, authStates }
+}
+
+export function buildActionContext(
+ client: KiloClient,
+ post: (msg: unknown) => void,
+ errFn: (err: unknown) => string,
+ dir: string,
+ refresh: () => Promise,
+): ActionContext {
+ return {
+ client,
+ postMessage: post,
+ getErrorMessage: errFn,
+ workspaceDir: dir,
+ disposeGlobal: async (reason: string) => {
+ await client.global.dispose().catch((error: unknown) => {
+ console.warn(`[Kilo New] KiloProvider: global.dispose() after ${reason} failed:`, error)
+ })
+ },
+ fetchAndSendProviders: refresh,
+ }
+}
+
+function isModelSelection(r: unknown): r is { providerID: string; modelID: string } {
+ return (
+ !!r &&
+ typeof r === "object" &&
+ typeof (r as Record).providerID === "string" &&
+ typeof (r as Record).modelID === "string"
+ )
+}
+
+/** Validate and sanitize recent model selections from untrusted sources. */
+export function validateRecents(raw: unknown): Array<{ providerID: string; modelID: string }> {
+ if (!Array.isArray(raw)) return []
+ return raw
+ .filter(isModelSelection)
+ .slice(0, 5)
+ .map((r) => ({ providerID: r.providerID, modelID: r.modelID }))
+}
+
+export function computeDefaultSelection(
+ cachedConfig: { config?: { model?: string } } | null,
+ vscodePID: string,
+ vscodeMID: string,
+): { providerID: string; modelID: string } {
+ const configured = parseModelString(cachedConfig?.config?.model)
+ if (configured) return configured
+ if (vscodePID && vscodeMID) return { providerID: vscodePID, modelID: vscodeMID }
+ return { ...KILO_AUTO }
+}
+
+type PostMessage = (message: unknown) => void
+type GetErrorMessage = (error: unknown) => string
+
+interface ActionContext {
+ client: KiloClient
+ postMessage: PostMessage
+ getErrorMessage: GetErrorMessage
+ workspaceDir: string
+ disposeGlobal: (reason: string) => Promise
+ fetchAndSendProviders: () => Promise
+}
+
+function postError(
+ ctx: ActionContext,
+ requestId: string,
+ providerID: string,
+ action: "connect" | "disconnect" | "authorize",
+ message: string,
+) {
+ ctx.postMessage({ type: "providerActionError", requestId, providerID, action, message })
+}
+
+function validateID(
+ ctx: ActionContext,
+ requestId: string,
+ providerID: string,
+ action: "connect" | "disconnect" | "authorize",
+): string | null {
+ const result = validateProviderIDShared(providerID)
+ if ("value" in result) return result.value
+ postError(ctx, requestId, providerID, action, result.error)
+ return null
+}
+
+export async function connectProvider(ctx: ActionContext, requestId: string, providerID: string, apiKey: string) {
+ const id = validateID(ctx, requestId, providerID, "connect")
+ if (!id) return
+ try {
+ await ctx.client.auth.set({ providerID: id, auth: { type: "api", key: apiKey } }, { throwOnError: true })
+ await ctx.disposeGlobal(`provider connect (${id})`)
+ await ctx.fetchAndSendProviders()
+ ctx.postMessage({ type: "providerConnected", requestId, providerID: id })
+ } catch (error) {
+ postError(ctx, requestId, providerID, "connect", ctx.getErrorMessage(error) || "Failed to connect provider")
+ }
+}
+
+export async function authorizeProviderOAuth(
+ ctx: ActionContext,
+ requestId: string,
+ providerID: string,
+ method: number,
+) {
+ const id = validateID(ctx, requestId, providerID, "authorize")
+ if (!id) return
+ try {
+ const { data: authorization } = await ctx.client.provider.oauth.authorize(
+ { providerID: id, method, directory: ctx.workspaceDir },
+ { throwOnError: true },
+ )
+ if (!authorization) {
+ postError(ctx, requestId, providerID, "authorize", "Failed to start provider authorization")
+ return
+ }
+ ctx.postMessage({ type: "providerOAuthReady", requestId, providerID: id, authorization })
+ } catch (error) {
+ postError(
+ ctx,
+ requestId,
+ providerID,
+ "authorize",
+ ctx.getErrorMessage(error) || "Failed to start provider authorization",
+ )
+ }
+}
+
+export async function completeProviderOAuth(
+ ctx: ActionContext,
+ requestId: string,
+ providerID: string,
+ method: number,
+ code?: string,
+) {
+ const id = validateID(ctx, requestId, providerID, "connect")
+ if (!id) return
+ try {
+ await ctx.client.provider.oauth.callback(
+ { providerID: id, method, code, directory: ctx.workspaceDir },
+ { throwOnError: true },
+ )
+ await ctx.disposeGlobal(`provider oauth (${id})`)
+ await ctx.fetchAndSendProviders()
+ ctx.postMessage({ type: "providerConnected", requestId, providerID: id })
+ } catch (error) {
+ postError(
+ ctx,
+ requestId,
+ providerID,
+ "connect",
+ ctx.getErrorMessage(error) || "Failed to complete provider authorization",
+ )
+ }
+}
+
+export async function disconnectProvider(
+ ctx: ActionContext,
+ requestId: string,
+ providerID: string,
+ cachedConfigMessage: unknown,
+ setCachedConfig: (msg: unknown) => void,
+) {
+ const id = validateID(ctx, requestId, providerID, "disconnect")
+ if (!id) return
+ try {
+ const globalConfig = (await ctx.client.global.config.get({ throwOnError: true })).data ?? {}
+ const configured = !!globalConfig.provider?.[id]
+
+ // Remove auth store entry. Config-sourced providers may not have an auth
+ // store entry (credentials come from config or env), so failure is non-fatal.
+ // For auth-only providers, failure means disconnect failed.
+ try {
+ await ctx.client.auth.remove({ providerID: id }, { throwOnError: true })
+ } catch (err) {
+ if (!configured) throw err
+ console.warn(`[Kilo New] auth.remove failed for configured provider ${id} (non-fatal):`, err)
+ }
+
+ if (id === "kilo") {
+ ctx.postMessage({ type: "profileData", data: null })
+ }
+
+ // Config-sourced providers stay "connected" after auth.remove because the
+ // server rebuilds state from config. Add to disabled_providers so the server
+ // excludes them. The config entry is preserved (user may re-enable later).
+ // This matches the desktop app's disableProvider() pattern.
+ if (configured) {
+ const disabled = globalConfig.disabled_providers ?? []
+ if (!disabled.includes(id)) {
+ const merged = (
+ await ctx.client.global.config.update(
+ { config: { disabled_providers: [...disabled, id] } },
+ { throwOnError: true },
+ )
+ ).data
+ if (merged) {
+ setCachedConfig({ type: "configLoaded", config: merged })
+ ctx.postMessage({ type: "configUpdated", config: merged })
+ }
+ }
+ }
+
+ await ctx.disposeGlobal(`provider disconnect (${id})`)
+ await ctx.fetchAndSendProviders()
+ ctx.postMessage({ type: "providerDisconnected", requestId, providerID: id })
+ } catch (error) {
+ postError(ctx, requestId, providerID, "disconnect", ctx.getErrorMessage(error) || "Failed to disconnect provider")
+ }
+}
+
+export async function saveCustomProvider(
+ ctx: ActionContext,
+ requestId: string,
+ providerID: string,
+ provider: Record,
+ apiKey: string | undefined,
+ cachedConfigMessage: unknown,
+ setCachedConfig: (msg: unknown) => void,
+) {
+ const id = validateID(ctx, requestId, providerID, "connect")
+ if (!id) return
+
+ const sanitized = sanitizeCustomProviderConfig(provider)
+ if ("error" in sanitized) {
+ postError(ctx, requestId, providerID, "connect", sanitized.error)
+ return
+ }
+
+ const refresh = async () => {
+ await ctx.disposeGlobal(`custom provider save (${id})`)
+ await ctx.fetchAndSendProviders()
+ }
+
+ try {
+ const globalConfig = (await ctx.client.global.config.get({ throwOnError: true })).data ?? {}
+ const disabled = globalConfig.disabled_providers ?? []
+ const nextDisabled = disabled.filter((item: string) => item !== id)
+ const { data: updated } = await ctx.client.global.config.update(
+ {
+ config: {
+ provider: { [id]: sanitized.value },
+ disabled_providers: nextDisabled,
+ },
+ },
+ { throwOnError: true },
+ )
+
+ const msg = { type: "configLoaded", config: updated }
+ setCachedConfig(msg)
+ ctx.postMessage({ type: "configUpdated", config: updated })
+
+ try {
+ if (apiKey) {
+ await ctx.client.auth.set({ providerID: id, auth: { type: "api", key: apiKey } }, { throwOnError: true })
+ } else {
+ await ctx.client.auth.remove({ providerID: id }, { throwOnError: true })
+ }
+ } catch (error) {
+ await refresh()
+ postError(ctx, requestId, providerID, "connect", ctx.getErrorMessage(error) || "Failed to save custom provider")
+ return
+ }
+
+ await refresh()
+ ctx.postMessage({ type: "providerConnected", requestId, providerID: id })
+ } catch (error) {
+ postError(ctx, requestId, providerID, "connect", ctx.getErrorMessage(error) || "Failed to save custom provider")
+ }
+}
diff --git a/packages/kilo-vscode/src/shared/custom-provider.ts b/packages/kilo-vscode/src/shared/custom-provider.ts
new file mode 100644
index 0000000000..61305e3c1b
--- /dev/null
+++ b/packages/kilo-vscode/src/shared/custom-provider.ts
@@ -0,0 +1,114 @@
+import { z } from "zod"
+import { CUSTOM_PROVIDER_PACKAGE, PROVIDER_ID_PATTERN } from "./provider-model"
+
+const INVALID_PROVIDER_ID = "Invalid provider ID"
+const INVALID_ENV = "Invalid environment variable name"
+const INVALID_BASE_URL = "Base URL must start with http:// or https://"
+
+export const ProviderIDSchema = z.string().trim().regex(PROVIDER_ID_PATTERN, INVALID_PROVIDER_ID)
+export const EnvSchema = z
+ .string()
+ .trim()
+ .regex(/^[A-Z_][A-Z0-9_]*$/, INVALID_ENV)
+export const CustomProviderConfigSchema = z
+ .object({
+ npm: z.string().optional(),
+ name: z.string().trim().min(1).max(200),
+ env: z.array(EnvSchema).max(1).optional(),
+ options: z
+ .object({
+ baseURL: z
+ .string()
+ .trim()
+ .url()
+ .refine((value) => value.startsWith("http://") || value.startsWith("https://"), {
+ message: INVALID_BASE_URL,
+ }),
+ headers: z.record(z.string().trim().min(1), z.string().trim().min(1)).optional(),
+ })
+ .strict(),
+ models: z
+ .record(
+ z.string().trim().min(1),
+ z
+ .object({
+ name: z.string().trim().min(1).max(200),
+ })
+ .strict(),
+ )
+ .refine((value) => Object.keys(value).length > 0, "At least one model is required"),
+ })
+ .strict()
+
+export type SanitizedProviderConfig = {
+ npm: typeof CUSTOM_PROVIDER_PACKAGE
+ name: string
+ env?: string[]
+ options: {
+ baseURL: string
+ headers?: Record
+ }
+ models: Record
+}
+
+type Issue = { error: string; issue?: z.ZodIssue }
+
+function fail(error: string, issue?: z.ZodIssue): Issue {
+ return issue ? { error, issue } : { error }
+}
+
+export function validateProviderID(providerID: string): { value: string } | Issue {
+ const result = ProviderIDSchema.safeParse(providerID)
+ if (result.success) return { value: result.data }
+ const issue = result.error.issues[0]
+ return fail(issue?.message ?? INVALID_PROVIDER_ID, issue)
+}
+
+export function parseCustomProviderSecret(raw: string): { value: { apiKey?: string; env?: string } } | Issue {
+ const value = raw.trim()
+ if (!value) return { value: {} }
+
+ const match = value.match(/^\{env:([^}]+)\}$/)
+ if (!match) return { value: { apiKey: value } }
+
+ const env = match[1]?.trim() ?? ""
+ const result = EnvSchema.safeParse(env)
+ if (result.success) return { value: { env: result.data } }
+ const issue = result.error.issues[0]
+ return fail(issue?.message ?? INVALID_ENV, issue)
+}
+
+export function normalizeCustomProviderConfig(
+ config: z.output,
+): SanitizedProviderConfig {
+ const headers = config.options.headers
+ ? Object.fromEntries(
+ Object.entries(config.options.headers)
+ .map(([key, value]) => [key.trim(), value.trim()] as const)
+ .filter(([key, value]) => key.length > 0 && value.length > 0),
+ )
+ : undefined
+
+ return {
+ npm: CUSTOM_PROVIDER_PACKAGE,
+ name: config.name.trim(),
+ ...(config.env ? { env: config.env.map((item) => item.trim()) } : {}),
+ options: {
+ baseURL: config.options.baseURL.trim(),
+ ...(headers && Object.keys(headers).length > 0 ? { headers } : {}),
+ },
+ models: Object.fromEntries(
+ Object.entries(config.models).map(([id, model]) => [id.trim(), { name: model.name.trim() }]),
+ ),
+ }
+}
+
+export function sanitizeCustomProviderConfig(provider: unknown): { value: SanitizedProviderConfig } | Issue {
+ const result = CustomProviderConfigSchema.safeParse(provider)
+ if (!result.success) {
+ const issue = result.error.issues[0]
+ return fail(issue?.message ?? "Invalid custom provider config", issue)
+ }
+
+ return { value: normalizeCustomProviderConfig(result.data) }
+}
diff --git a/packages/kilo-vscode/src/shared/provider-model.ts b/packages/kilo-vscode/src/shared/provider-model.ts
new file mode 100644
index 0000000000..21de283075
--- /dev/null
+++ b/packages/kilo-vscode/src/shared/provider-model.ts
@@ -0,0 +1,36 @@
+export const KILO_PROVIDER_ID = "kilo"
+export const KILO_AUTO = { providerID: KILO_PROVIDER_ID, modelID: "kilo-auto/free" } as const
+export const CUSTOM_PROVIDER_PACKAGE = "@ai-sdk/openai-compatible"
+export const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-_]*$/
+
+export const PROVIDER_PRIORITY = [
+ KILO_PROVIDER_ID,
+ "anthropic",
+ "github-copilot",
+ "openai",
+ "google",
+ "openrouter",
+ "vercel",
+] as const
+
+export function parseModelString(raw: string | undefined | null) {
+ if (!raw) return null
+ const slash = raw.indexOf("/")
+ if (slash <= 0 || slash >= raw.length - 1) return null
+ return { providerID: raw.slice(0, slash), modelID: raw.slice(slash + 1) }
+}
+
+export function providerOrderIndex(providerID: string, order = PROVIDER_PRIORITY) {
+ const index = order.indexOf(providerID.toLowerCase() as (typeof PROVIDER_PRIORITY)[number])
+ return index >= 0 ? index : order.length
+}
+
+export function createKiloFallbackProvider() {
+ return {
+ id: KILO_PROVIDER_ID,
+ name: "Kilo Gateway",
+ source: "custom" as const,
+ env: ["KILO_API_KEY"],
+ models: {},
+ }
+}
diff --git a/packages/kilo-vscode/tests/unit/custom-provider.test.ts b/packages/kilo-vscode/tests/unit/custom-provider.test.ts
new file mode 100644
index 0000000000..d80c98a709
--- /dev/null
+++ b/packages/kilo-vscode/tests/unit/custom-provider.test.ts
@@ -0,0 +1,83 @@
+import { describe, expect, it } from "bun:test"
+import {
+ parseCustomProviderSecret,
+ sanitizeCustomProviderConfig,
+ validateProviderID,
+} from "../../src/shared/custom-provider"
+
+describe("validateProviderID", () => {
+ it("accepts valid provider ids", () => {
+ expect(validateProviderID(" my-provider_1 ")).toEqual({ value: "my-provider_1" })
+ })
+
+ it("rejects invalid provider ids", () => {
+ const result = validateProviderID("bad/id")
+ expect("error" in result ? result.error : "").toBe("Invalid provider ID")
+ })
+})
+
+describe("parseCustomProviderSecret", () => {
+ it("treats plain values as api keys", () => {
+ expect(parseCustomProviderSecret(" sk-test ")).toEqual({ value: { apiKey: "sk-test" } })
+ })
+
+ it("parses env references", () => {
+ expect(parseCustomProviderSecret(" {env:MY_PROVIDER_KEY} ")).toEqual({ value: { env: "MY_PROVIDER_KEY" } })
+ })
+
+ it("rejects invalid env references", () => {
+ const result = parseCustomProviderSecret("{env:bad-name}")
+ expect("error" in result ? result.error : "").toBe("Invalid environment variable name")
+ })
+})
+
+describe("sanitizeCustomProviderConfig", () => {
+ it("normalizes config and forces the approved package", () => {
+ const result = sanitizeCustomProviderConfig({
+ npm: "malicious-package",
+ name: " My Provider ",
+ env: [" MY_PROVIDER_KEY "],
+ options: {
+ baseURL: "https://example.com/v1 ",
+ headers: {
+ Authorization: " Bearer test ",
+ " X-Test ": " 123 ",
+ },
+ },
+ models: {
+ " model-1 ": { name: " Model One " },
+ },
+ })
+
+ expect(result).toEqual({
+ value: {
+ npm: "@ai-sdk/openai-compatible",
+ name: "My Provider",
+ env: ["MY_PROVIDER_KEY"],
+ options: {
+ baseURL: "https://example.com/v1",
+ headers: {
+ Authorization: "Bearer test",
+ "X-Test": "123",
+ },
+ },
+ models: {
+ "model-1": { name: "Model One" },
+ },
+ },
+ })
+ })
+
+ it("rejects unknown fields", () => {
+ const result = sanitizeCustomProviderConfig({
+ name: "Bad Provider",
+ options: {
+ baseURL: "https://example.com/v1",
+ mcpServer: "https://malicious.example",
+ },
+ models: { "model-1": { name: "Model One" } },
+ })
+
+ expect("error" in result ? result.error : "").toContain("mcpServer")
+ })
+})
diff --git a/packages/kilo-vscode/tests/unit/model-selection.test.ts b/packages/kilo-vscode/tests/unit/model-selection.test.ts
new file mode 100644
index 0000000000..c61d1df1db
--- /dev/null
+++ b/packages/kilo-vscode/tests/unit/model-selection.test.ts
@@ -0,0 +1,106 @@
+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" })
+ })
+})
diff --git a/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts b/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts
index b331ee3926..2f70635778 100644
--- a/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts
+++ b/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts
@@ -16,8 +16,8 @@ describe("providerSortKey", () => {
it("returns correct index for known providers", () => {
expect(providerSortKey("anthropic")).toBe(1)
- expect(providerSortKey("openai")).toBe(2)
- expect(providerSortKey("google")).toBe(3)
+ expect(providerSortKey("openai")).toBe(3)
+ expect(providerSortKey("google")).toBe(4)
})
it("returns order length for unknown provider", () => {
@@ -37,9 +37,9 @@ describe("providerSortKey", () => {
})
it("sorts providers correctly when used with sort", () => {
- const ids = ["google", "anthropic", "kilo", "openai"]
+ const ids = ["google", "anthropic", "kilo", "openai", "github-copilot"]
const sorted = ids.slice().sort((a, b) => providerSortKey(a) - providerSortKey(b))
- expect(sorted).toEqual(["kilo", "anthropic", "openai", "google"])
+ expect(sorted).toEqual(["kilo", "anthropic", "github-copilot", "openai", "google"])
})
})
@@ -62,63 +62,69 @@ describe("stripSubProviderPrefix", () => {
describe("buildTriggerLabel", () => {
it("returns resolved model name for non-kilo provider unchanged", () => {
- expect(buildTriggerLabel("GPT-4o", "openai", null, false, "", true, labels)).toBe("GPT-4o")
+ expect(buildTriggerLabel("GPT-4o", "openai", undefined, null, false, "", true, labels)).toBe("GPT-4o")
})
it("strips sub-provider prefix from resolved name for kilo gateway models", () => {
- expect(buildTriggerLabel("Anthropic: Claude Sonnet", KILO_GATEWAY_ID, null, false, "", true, labels)).toBe(
- "Claude Sonnet",
- )
+ expect(
+ buildTriggerLabel("Anthropic: Claude Sonnet", KILO_GATEWAY_ID, undefined, null, false, "", true, labels),
+ ).toBe("Claude Sonnet")
})
it("does not strip prefix for non-kilo provider even if name contains ': '", () => {
- expect(buildTriggerLabel("Anthropic: Claude Sonnet", "anthropic", null, false, "", true, labels)).toBe(
+ expect(buildTriggerLabel("Anthropic: Claude Sonnet", "anthropic", undefined, null, false, "", true, labels)).toBe(
"Anthropic: Claude Sonnet",
)
})
it("returns resolved name as-is when providerID is undefined", () => {
- expect(buildTriggerLabel("GPT-4o", undefined, null, false, "", true, labels)).toBe("GPT-4o")
+ expect(buildTriggerLabel("GPT-4o", undefined, undefined, null, false, "", true, labels)).toBe("GPT-4o")
+ })
+
+ it("returns providerName / resolvedName for non-kilo provider with providerName", () => {
+ expect(buildTriggerLabel("GPT-4o", "openai", "OpenAI", null, false, "", true, labels)).toBe("OpenAI / GPT-4o")
})
it("returns modelID for kilo gateway raw selection", () => {
const raw = { providerID: "kilo", modelID: "kilo-auto/frontier" }
- expect(buildTriggerLabel(undefined, undefined, raw, false, "", true, labels)).toBe("kilo-auto/frontier")
+ expect(buildTriggerLabel(undefined, undefined, undefined, raw, false, "", true, labels)).toBe("kilo-auto/frontier")
})
it("returns providerID / modelID for non-kilo raw selection", () => {
const raw = { providerID: "anthropic", modelID: "claude-3-5-sonnet" }
- expect(buildTriggerLabel(undefined, undefined, raw, false, "", true, labels)).toBe("anthropic / claude-3-5-sonnet")
+ expect(buildTriggerLabel(undefined, undefined, undefined, raw, false, "", true, labels)).toBe(
+ "anthropic / claude-3-5-sonnet",
+ )
})
it("returns clearLabel when allowClear and no selection", () => {
- expect(buildTriggerLabel(undefined, undefined, null, true, "None", true, labels)).toBe("None")
+ expect(buildTriggerLabel(undefined, undefined, undefined, null, true, "None", true, labels)).toBe("None")
})
it("falls back to labels.notSet when allowClear and clearLabel is empty", () => {
- expect(buildTriggerLabel(undefined, undefined, null, true, "", true, labels)).toBe("Not set")
+ expect(buildTriggerLabel(undefined, undefined, undefined, null, true, "", true, labels)).toBe("Not set")
})
it("returns labels.select when providers exist and no selection", () => {
- expect(buildTriggerLabel(undefined, undefined, null, false, "", true, labels)).toBe("Select model")
+ expect(buildTriggerLabel(undefined, undefined, undefined, null, false, "", true, labels)).toBe("Select model")
})
it("returns labels.noProviders when no providers available", () => {
- expect(buildTriggerLabel(undefined, undefined, null, false, "", false, labels)).toBe("No providers")
+ expect(buildTriggerLabel(undefined, undefined, undefined, null, false, "", false, labels)).toBe("No providers")
})
it("prefers resolvedName over raw selection", () => {
const raw = { providerID: "anthropic", modelID: "claude-3-5-sonnet" }
- expect(buildTriggerLabel("Claude Sonnet", undefined, raw, false, "", true, labels)).toBe("Claude Sonnet")
+ expect(buildTriggerLabel("Claude Sonnet", undefined, undefined, raw, false, "", true, labels)).toBe("Claude Sonnet")
})
it("ignores partial raw selection (only providerID)", () => {
const raw = { providerID: "anthropic", modelID: "" }
- expect(buildTriggerLabel(undefined, undefined, raw, false, "", true, labels)).toBe("Select model")
+ expect(buildTriggerLabel(undefined, undefined, undefined, raw, false, "", true, labels)).toBe("Select model")
})
it("ignores partial raw selection (only modelID)", () => {
const raw = { providerID: "", modelID: "claude-3-5-sonnet" }
- expect(buildTriggerLabel(undefined, undefined, raw, false, "", true, labels)).toBe("Select model")
+ expect(buildTriggerLabel(undefined, undefined, undefined, raw, false, "", true, labels)).toBe("Select model")
})
})
diff --git a/packages/kilo-vscode/tests/unit/provider-action.test.ts b/packages/kilo-vscode/tests/unit/provider-action.test.ts
new file mode 100644
index 0000000000..c3c498250c
--- /dev/null
+++ b/packages/kilo-vscode/tests/unit/provider-action.test.ts
@@ -0,0 +1,143 @@
+import { describe, expect, it } from "bun:test"
+import { createProviderAction } from "../../webview-ui/src/utils/provider-action"
+import type { ExtensionMessage, WebviewMessage } from "../../webview-ui/src/types/messages"
+
+function createTransport() {
+ const sent: WebviewMessage[] = []
+ let handler: ((message: ExtensionMessage) => void) | undefined
+
+ return {
+ sent,
+ receive(message: ExtensionMessage) {
+ handler?.(message)
+ },
+ postMessage(message: WebviewMessage) {
+ sent.push(message)
+ },
+ onMessage(next: (message: ExtensionMessage) => void) {
+ handler = next
+ return () => {
+ if (handler === next) {
+ handler = undefined
+ }
+ }
+ },
+ }
+}
+
+describe("createProviderAction", () => {
+ it("routes terminal provider messages by request id", () => {
+ const transport = createTransport()
+ const action = createProviderAction(transport)
+ const seen: string[] = []
+
+ action.send(
+ {
+ type: "connectProvider",
+ providerID: "openai",
+ apiKey: "sk-test",
+ },
+ {
+ onConnected: (message) => seen.push(`connected:${message.providerID}`),
+ },
+ )
+
+ const sent = transport.sent[0]
+ expect(sent?.type).toBe("connectProvider")
+ expect("requestId" in (sent ?? {}) ? sent.requestId : "").toBeString()
+
+ const requestId = "requestId" in (sent ?? {}) ? sent.requestId : ""
+ transport.receive({
+ type: "providerConnected",
+ requestId,
+ providerID: "openai",
+ })
+ transport.receive({
+ type: "providerConnected",
+ requestId,
+ providerID: "openai",
+ })
+
+ expect(seen).toEqual(["connected:openai"])
+ action.dispose()
+ })
+
+ it("keeps concurrent requests isolated", () => {
+ const transport = createTransport()
+ const action = createProviderAction(transport)
+ const seen: string[] = []
+
+ action.send(
+ {
+ type: "authorizeProviderOAuth",
+ providerID: "anthropic",
+ method: 0,
+ },
+ {
+ onOAuthReady: (message) => seen.push(`oauth:${message.authorization.method}`),
+ },
+ )
+ action.send(
+ {
+ type: "disconnectProvider",
+ providerID: "openai",
+ },
+ {
+ onDisconnected: (message) => seen.push(`disconnect:${message.providerID}`),
+ },
+ )
+
+ const oauth = transport.sent[0]
+ const disconnect = transport.sent[1]
+ const oauthId = "requestId" in (oauth ?? {}) ? oauth.requestId : ""
+ const disconnectId = "requestId" in (disconnect ?? {}) ? disconnect.requestId : ""
+
+ transport.receive({
+ type: "providerDisconnected",
+ requestId: disconnectId,
+ providerID: "openai",
+ })
+ transport.receive({
+ type: "providerOAuthReady",
+ requestId: oauthId,
+ providerID: "anthropic",
+ authorization: { url: "https://example.com", method: "code", instructions: "Code: 1234" },
+ })
+
+ expect(seen).toEqual(["disconnect:openai", "oauth:code"])
+ action.dispose()
+ })
+
+ it("can drop stale requests", () => {
+ const transport = createTransport()
+ const action = createProviderAction(transport)
+ const seen: string[] = []
+
+ const requestId = action.send(
+ {
+ type: "saveCustomProvider",
+ providerID: "myprovider",
+ config: {
+ name: "My Provider",
+ options: { baseURL: "https://example.com/v1" },
+ models: { "model-1": { name: "Model One" } },
+ },
+ },
+ {
+ onError: (message) => seen.push(message.message),
+ },
+ )
+
+ action.clear(requestId)
+ transport.receive({
+ type: "providerActionError",
+ requestId,
+ providerID: "myprovider",
+ action: "connect",
+ message: "boom",
+ })
+
+ expect(seen).toEqual([])
+ action.dispose()
+ })
+})
diff --git a/packages/kilo-vscode/tests/unit/provider-utils.test.ts b/packages/kilo-vscode/tests/unit/provider-utils.test.ts
index 315a589f9a..c8eab7e588 100644
--- a/packages/kilo-vscode/tests/unit/provider-utils.test.ts
+++ b/packages/kilo-vscode/tests/unit/provider-utils.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from "bun:test"
-import { flattenModels, findModel } from "../../webview-ui/src/context/provider-utils"
+import { flattenModels, findModel, isModelValid } from "../../webview-ui/src/context/provider-utils"
import type { Provider } from "../../webview-ui/src/types/messages"
function makeProvider(id: string, name: string, modelIds: string[]): Provider {
@@ -78,3 +78,26 @@ describe("findModel", () => {
expect(findModel([], { providerID: "openai", modelID: "gpt-4" })).toBeUndefined()
})
})
+
+describe("isModelValid", () => {
+ const providers = {
+ kilo: makeProvider("kilo", "Kilo Gateway", ["kilo-auto/free"]),
+ openai: makeProvider("openai", "OpenAI", ["gpt-4o"]),
+ }
+
+ it("accepts a connected provider model", () => {
+ expect(isModelValid(providers, ["openai"], { providerID: "openai", modelID: "gpt-4o" })).toBe(true)
+ })
+
+ it("rejects a disconnected non-kilo provider", () => {
+ expect(isModelValid(providers, [], { providerID: "openai", modelID: "gpt-4o" })).toBe(false)
+ })
+
+ it("accepts kilo models when present in the catalog", () => {
+ expect(isModelValid(providers, [], { providerID: "kilo", modelID: "kilo-auto/free" })).toBe(true)
+ })
+
+ it("rejects unknown models", () => {
+ expect(isModelValid(providers, ["openai"], { providerID: "openai", modelID: "missing" })).toBe(false)
+ })
+})
diff --git a/packages/kilo-vscode/tests/unit/provider-visibility.test.ts b/packages/kilo-vscode/tests/unit/provider-visibility.test.ts
new file mode 100644
index 0000000000..5b71ad7ef9
--- /dev/null
+++ b/packages/kilo-vscode/tests/unit/provider-visibility.test.ts
@@ -0,0 +1,23 @@
+import { describe, expect, it } from "bun:test"
+
+import { visibleConnectedIds } from "../../webview-ui/src/components/settings/provider-visibility"
+
+describe("visibleConnectedIds", () => {
+ it("hides Kilo from the connected list when auth is missing", () => {
+ const ids = visibleConnectedIds(["kilo", "openrouter"], { openrouter: "api" })
+
+ expect(ids).toEqual(["openrouter"])
+ })
+
+ it("keeps Kilo in the connected list when auth exists", () => {
+ const ids = visibleConnectedIds(["kilo", "openrouter"], { kilo: "oauth", openrouter: "api" })
+
+ expect(ids).toEqual(["kilo", "openrouter"])
+ })
+
+ it("leaves non-Kilo providers untouched", () => {
+ const ids = visibleConnectedIds(["anthropic"], {})
+
+ expect(ids).toEqual(["anthropic"])
+ })
+})
diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/providers-configure-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/providers-configure-chromium-linux.png
index 58ecfff2a4..d4443c43e7 100644
--- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/providers-configure-chromium-linux.png
+++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/providers-configure-chromium-linux.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b7fe365d1a0ffdd713d3146c66112fad2207c2030132ab117543a6a430197aca
-size 36247
+oid sha256:19d68a55c4a80d83ba70770744764ab33b6a5291ff02a34106a12d0935203ffa
+size 20185
diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/settings-panel-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/settings-panel-chromium-linux.png
index 0412526b18..ed6030cc17 100644
--- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/settings-panel-chromium-linux.png
+++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/settings/settings-panel-chromium-linux.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b83a16d340c4af266d4dd0134a840c7c07bed50837a58460d3cebab61ca2bd06
-size 45545
+oid sha256:3ad2458e2fe4e735c09fe89292c238f4b3113d2fa7dab5353889f0fc885400a7
+size 30038
diff --git a/packages/kilo-vscode/webview-ui/src/components/profile/DeviceAuthCard.tsx b/packages/kilo-vscode/webview-ui/src/components/profile/DeviceAuthCard.tsx
index 4befc8fc06..aebcdec4f5 100644
--- a/packages/kilo-vscode/webview-ui/src/components/profile/DeviceAuthCard.tsx
+++ b/packages/kilo-vscode/webview-ui/src/components/profile/DeviceAuthCard.tsx
@@ -3,6 +3,8 @@ import { Button } from "@kilocode/kilo-ui/button"
import { Card } from "@kilocode/kilo-ui/card"
import { Spinner } from "@kilocode/kilo-ui/spinner"
import { showToast } from "@kilocode/kilo-ui/toast"
+import { useDialog } from "@kilocode/kilo-ui/context/dialog"
+import { Dialog } from "@kilocode/kilo-ui/dialog"
import { useVSCode } from "../../context/vscode"
import { useLanguage } from "../../context/language"
import { generateQRCode } from "../../utils/qrcode"
@@ -18,15 +20,41 @@ interface DeviceAuthCardProps {
onRetry: () => void
}
+const ERROR_LIMIT = 180
+
const formatTime = (seconds: number): string => {
const m = Math.floor(seconds / 60)
const s = seconds % 60
return `${m}:${s.toString().padStart(2, "0")}`
}
+function compactError(error: string | undefined, fallback: string) {
+ if (!error) return fallback
+
+ const text = error.replace(/\s+/g, " ").trim()
+ const html = text.search(/= 0
+ ? text
+ .slice(0, html)
+ .trim()
+ .replace(/[\s:,-]+$/, "")
+ : text
+
+ if (head.length > 0) {
+ if (head.length <= ERROR_LIMIT) return head
+ return `${head.slice(0, ERROR_LIMIT).trimEnd()}...`
+ }
+
+ const status = text.match(/\b([45]\d{2})\b/)?.[1]
+ if (status) return `${fallback} (${status})`
+ return fallback
+}
+
const DeviceAuthCard: Component = (props) => {
const vscode = useVSCode()
const language = useLanguage()
+ const dialog = useDialog()
const [timeRemaining, setTimeRemaining] = createSignal(props.expiresIn ?? 900)
const [qrDataUrl, setQrDataUrl] = createSignal("")
@@ -70,6 +98,56 @@ const DeviceAuthCard: Component = (props) => {
}
}
+ const errorSummary = () => compactError(props.error, language.t("deviceAuth.status.failed"))
+
+ const hasErrorDetails = () => {
+ if (!props.error) return false
+ return errorSummary() !== props.error.replace(/\s+/g, " ").trim()
+ }
+
+ const handleCopyError = () => {
+ if (!props.error) return
+ navigator.clipboard.writeText(props.error)
+ showToast({ variant: "success", title: language.t("deviceAuth.toast.errorCopied") })
+ }
+
+ const handleShowError = () => {
+ if (!props.error) return
+
+ dialog.show(() => (
+
+ ))
+ }
+
return (
{/* Initiating state */}
@@ -281,11 +359,23 @@ const DeviceAuthCard: Component = (props) => {
margin: "8px 0 12px 0",
}}
>
- {props.error || language.t("deviceAuth.status.failed")}
+ {errorSummary()}
-
+
+
+
+
+
+
+
+
+
diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx
new file mode 100644
index 0000000000..a1debefb01
--- /dev/null
+++ b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx
@@ -0,0 +1,458 @@
+import { Button } from "@kilocode/kilo-ui/button"
+import { useDialog } from "@kilocode/kilo-ui/context/dialog"
+import { Dialog } from "@kilocode/kilo-ui/dialog"
+import { IconButton } from "@kilocode/kilo-ui/icon-button"
+import { ProviderIcon } from "@kilocode/kilo-ui/provider-icon"
+import { TextField } from "@kilocode/kilo-ui/text-field"
+import { showToast } from "@kilocode/kilo-ui/toast"
+import { For, onCleanup } from "solid-js"
+import { createStore } from "solid-js/store"
+import { useConfig } from "../../context/config"
+import { useLanguage } from "../../context/language"
+import { useProvider } from "../../context/provider"
+import { useVSCode } from "../../context/vscode"
+import { createProviderAction } from "../../utils/provider-action"
+
+const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/
+const OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible"
+
+type Translator = ReturnType["t"]
+
+type ModelRow = {
+ id: string
+ name: string
+}
+
+type HeaderRow = {
+ key: string
+ value: string
+}
+
+type FormState = {
+ providerID: string
+ name: string
+ baseURL: string
+ apiKey: string
+ models: ModelRow[]
+ headers: HeaderRow[]
+ saving: boolean
+}
+
+type FormErrors = {
+ providerID: string | undefined
+ name: string | undefined
+ baseURL: string | undefined
+ models: Array<{ id?: string; name?: string }>
+ headers: Array<{ key?: string; value?: string }>
+}
+
+type ValidateArgs = {
+ form: FormState
+ t: Translator
+ disabledProviders: string[]
+ existingProviderIDs: Set
+}
+
+function validateCustomProvider(input: ValidateArgs) {
+ const providerID = input.form.providerID.trim()
+ const name = input.form.name.trim()
+ const baseURL = input.form.baseURL.trim()
+ const apiKey = input.form.apiKey.trim()
+
+ const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim()
+ const key = apiKey && !env ? apiKey : undefined
+
+ const idError = !providerID
+ ? input.t("provider.custom.error.providerID.required")
+ : !PROVIDER_ID.test(providerID)
+ ? input.t("provider.custom.error.providerID.format")
+ : undefined
+
+ const nameError = !name ? input.t("provider.custom.error.name.required") : undefined
+ const urlError = !baseURL
+ ? input.t("provider.custom.error.baseURL.required")
+ : !/^https?:\/\//.test(baseURL)
+ ? input.t("provider.custom.error.baseURL.format")
+ : undefined
+
+ const disabled = input.disabledProviders.includes(providerID)
+ const existsError = idError
+ ? undefined
+ : input.existingProviderIDs.has(providerID) && !disabled
+ ? input.t("provider.custom.error.providerID.exists")
+ : undefined
+
+ const seenModels = new Set()
+ const modelErrors = input.form.models.map((m) => {
+ const id = m.id.trim()
+ const modelIdError = !id
+ ? input.t("provider.custom.error.required")
+ : seenModels.has(id)
+ ? input.t("provider.custom.error.duplicate")
+ : (() => {
+ seenModels.add(id)
+ return undefined
+ })()
+ const modelNameError = !m.name.trim() ? input.t("provider.custom.error.required") : undefined
+ return { id: modelIdError, name: modelNameError }
+ })
+ const modelsValid = modelErrors.every((m) => !m.id && !m.name)
+ const models = Object.fromEntries(input.form.models.map((m) => [m.id.trim(), { name: m.name.trim() }]))
+
+ const seenHeaders = new Set()
+ const headerErrors = input.form.headers.map((h) => {
+ const key = h.key.trim()
+ const value = h.value.trim()
+
+ if (!key && !value) return {}
+ const keyError = !key
+ ? input.t("provider.custom.error.required")
+ : seenHeaders.has(key.toLowerCase())
+ ? input.t("provider.custom.error.duplicate")
+ : (() => {
+ seenHeaders.add(key.toLowerCase())
+ return undefined
+ })()
+ const valueError = !value ? input.t("provider.custom.error.required") : undefined
+ return { key: keyError, value: valueError }
+ })
+ const headersValid = headerErrors.every((h) => !h.key && !h.value)
+ const headers = Object.fromEntries(
+ input.form.headers
+ .map((h) => ({ key: h.key.trim(), value: h.value.trim() }))
+ .filter((h) => !!h.key && !!h.value)
+ .map((h) => [h.key, h.value]),
+ )
+
+ const errors: FormErrors = {
+ providerID: idError ?? existsError,
+ name: nameError,
+ baseURL: urlError,
+ models: modelErrors,
+ headers: headerErrors,
+ }
+
+ const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid
+ if (!ok) return { errors }
+
+ const options = {
+ baseURL,
+ ...(Object.keys(headers).length ? { headers } : {}),
+ }
+
+ return {
+ errors,
+ result: {
+ providerID,
+ name,
+ key,
+ config: {
+ npm: OPENAI_COMPATIBLE,
+ name,
+ ...(env ? { env: [env] } : {}),
+ options,
+ models,
+ },
+ },
+ }
+}
+
+interface CustomProviderDialogProps {
+ onBack?: () => void
+}
+
+const CustomProviderDialog = (props: CustomProviderDialogProps) => {
+ const dialog = useDialog()
+ const { config } = useConfig()
+ const provider = useProvider()
+ const language = useLanguage()
+ const vscode = useVSCode()
+ const action = createProviderAction(vscode)
+ onCleanup(action.dispose)
+
+ const [form, setForm] = createStore({
+ providerID: "",
+ name: "",
+ baseURL: "",
+ apiKey: "",
+ models: [{ id: "", name: "" }],
+ headers: [{ key: "", value: "" }],
+ saving: false,
+ })
+
+ const [errors, setErrors] = createStore({
+ providerID: undefined,
+ name: undefined,
+ baseURL: undefined,
+ models: [{}],
+ headers: [{}],
+ })
+
+ function goBack() {
+ if (props.onBack) {
+ props.onBack()
+ return
+ }
+ dialog.close()
+ }
+
+ function addModel() {
+ setForm("models", (v) => [...v, { id: "", name: "" }])
+ setErrors("models", (v) => [...v, {}])
+ }
+
+ function removeModel(index: number) {
+ if (form.models.length <= 1) return
+ setForm("models", (v) => v.filter((_, i) => i !== index))
+ setErrors("models", (v) => v.filter((_, i) => i !== index))
+ }
+
+ function addHeader() {
+ setForm("headers", (v) => [...v, { key: "", value: "" }])
+ setErrors("headers", (v) => [...v, {}])
+ }
+
+ function removeHeader(index: number) {
+ if (form.headers.length <= 1) return
+ setForm("headers", (v) => v.filter((_, i) => i !== index))
+ setErrors("headers", (v) => v.filter((_, i) => i !== index))
+ }
+
+ function validate() {
+ const output = validateCustomProvider({
+ form,
+ t: language.t,
+ disabledProviders: config().disabled_providers ?? [],
+ existingProviderIDs: new Set(Object.keys(provider.providers())),
+ })
+ setErrors(output.errors)
+ return output.result
+ }
+
+ function save(e: SubmitEvent) {
+ e.preventDefault()
+ if (form.saving) return
+
+ const result = validate()
+ if (!result) return
+
+ setForm("saving", true)
+
+ action.send(
+ {
+ type: "saveCustomProvider",
+ providerID: result.providerID,
+ config: result.config,
+ apiKey: result.key,
+ },
+ {
+ onConnected: () => {
+ setForm("saving", false)
+ dialog.close()
+ showToast({
+ variant: "success",
+ icon: "circle-check",
+ title: language.t("provider.connect.toast.connected.title", { provider: result.name }),
+ description: language.t("provider.connect.toast.connected.description", { provider: result.name }),
+ })
+ },
+ onError: (message) => {
+ setForm("saving", false)
+ showToast({ title: language.t("common.requestFailed"), description: message.message })
+ },
+ },
+ )
+ }
+
+ return (
+
+ }
+ transition
+ >
+
+
+
+
+ {language.t("provider.custom.title")}
+
+
+
+
+
+
+ )
+}
+
+export default CustomProviderDialog
diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx
new file mode 100644
index 0000000000..8e0afb12fd
--- /dev/null
+++ b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx
@@ -0,0 +1,90 @@
+import { Component, For, createMemo } from "solid-js"
+import { Card } from "@kilocode/kilo-ui/card"
+import { useConfig } from "../../context/config"
+import { useLanguage } from "../../context/language"
+import { useSession } from "../../context/session"
+import { parseModelString } from "../../../../src/shared/provider-model"
+import { ModelSelectorBase } from "../shared/ModelSelector"
+import SettingsRow from "./SettingsRow"
+
+const ModelsTab: Component = () => {
+ const { config, updateConfig } = useConfig()
+ const language = useLanguage()
+ const session = useSession()
+
+ function handleModelSelect(configKey: "model" | "small_model") {
+ return (providerID: string, modelID: string) => {
+ if (!providerID || !modelID) {
+ updateConfig({ [configKey]: null })
+ return
+ }
+ updateConfig({ [configKey]: `${providerID}/${modelID}` })
+ }
+ }
+
+ const allAgents = createMemo(() => session.agents())
+
+ function handleModeModelSelect(agentName: string) {
+ return (providerID: string, modelID: string) => {
+ if (!providerID || !modelID) {
+ updateConfig({ agent: { [agentName]: { model: null } } })
+ return
+ }
+ updateConfig({ agent: { [agentName]: { model: `${providerID}/${modelID}` } } })
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
{language.t("settings.providers.modeModels")}
+
+
+ {(agent, index) => (
+
+
+
+ )}
+
+
+
+ )
+}
+
+export default ModelsTab
diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ProviderConnectDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ProviderConnectDialog.tsx
new file mode 100644
index 0000000000..145580f7ab
--- /dev/null
+++ b/packages/kilo-vscode/webview-ui/src/components/settings/ProviderConnectDialog.tsx
@@ -0,0 +1,406 @@
+import { Button } from "@kilocode/kilo-ui/button"
+import { useDialog } from "@kilocode/kilo-ui/context/dialog"
+import { Dialog } from "@kilocode/kilo-ui/dialog"
+import { Spinner } from "@kilocode/kilo-ui/spinner"
+import { TextField } from "@kilocode/kilo-ui/text-field"
+import { showToast } from "@kilocode/kilo-ui/toast"
+import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@kilocode/sdk/v2/client"
+import { Component, For, Match, Show, Switch, createMemo, createSignal, onCleanup, onMount } from "solid-js"
+import { createStore } from "solid-js/store"
+import { useLanguage } from "../../context/language"
+import { useProvider } from "../../context/provider"
+import { useVSCode } from "../../context/vscode"
+import { createProviderAction } from "../../utils/provider-action"
+
+interface ProviderConnectDialogProps {
+ providerID: string
+}
+
+interface ViewState {
+ methodIndex?: number
+ authorization?: ProviderAuthAuthorization
+ phase?: "authorizing" | "connecting"
+ error?: string
+ failed?: string
+}
+
+function fallbackMethods(label: string): ProviderAuthMethod[] {
+ return [{ type: "api", label }]
+}
+
+function formatError(value: unknown, fallback: string): string {
+ if (value && typeof value === "object" && "message" in value) {
+ const message = (value as { message?: unknown }).message
+ if (typeof message === "string" && message) return message
+ }
+ if (typeof value === "string" && value) return value
+ return fallback
+}
+
+const ProviderConnectDialog: Component = (props) => {
+ const dialog = useDialog()
+ const language = useLanguage()
+ const provider = useProvider()
+ const vscode = useVSCode()
+ const action = createProviderAction(vscode)
+
+ const [state, setState] = createStore({})
+
+ const item = createMemo(() => provider.providers()[props.providerID])
+ const name = () => item()?.name ?? props.providerID
+ const methods = createMemo(() => {
+ return provider.authMethods()[props.providerID] ?? fallbackMethods(language.t("provider.connect.method.apiKey"))
+ })
+ const method = createMemo(() => {
+ const index = state.methodIndex
+ return index === undefined ? undefined : methods()[index]
+ })
+
+ onCleanup(action.dispose)
+
+ onMount(() => {
+ if (methods().length !== 1) return
+ selectMethod(0)
+ })
+
+ function openExternal(url: string) {
+ vscode.postMessage({ type: "openExternal", url })
+ }
+
+ function reset() {
+ action.clear()
+ setState({
+ methodIndex: undefined,
+ authorization: undefined,
+ phase: undefined,
+ error: undefined,
+ failed: undefined,
+ })
+ }
+
+ function fail(message: string) {
+ const failed = state.authorization?.method === "auto" || state.phase === "authorizing"
+ setState({
+ ...state,
+ phase: undefined,
+ error: failed ? undefined : message,
+ failed: failed ? message : undefined,
+ })
+ }
+
+ function succeed() {
+ showToast({
+ variant: "success",
+ icon: "circle-check",
+ title: language.t("provider.connect.toast.connected.title", { provider: name() }),
+ description: language.t("provider.connect.toast.connected.description", { provider: name() }),
+ })
+ dialog.close()
+ }
+
+ function selectMethod(index: number) {
+ const current = methods()[index]
+ action.clear()
+ setState({
+ methodIndex: index,
+ authorization: undefined,
+ phase: current?.type === "oauth" ? "authorizing" : undefined,
+ error: undefined,
+ failed: undefined,
+ })
+ if (current?.type !== "oauth") return
+
+ action.send(
+ {
+ type: "authorizeProviderOAuth",
+ providerID: props.providerID,
+ method: index,
+ },
+ {
+ onOAuthReady: (message) => {
+ setState({
+ ...state,
+ authorization: message.authorization,
+ phase: undefined,
+ error: undefined,
+ failed: undefined,
+ })
+ },
+ onError: (message) => fail(message.message),
+ },
+ )
+ }
+
+ function connect(apiKey: string) {
+ setState({
+ ...state,
+ phase: "connecting",
+ error: undefined,
+ failed: undefined,
+ })
+ action.send(
+ {
+ type: "connectProvider",
+ providerID: props.providerID,
+ apiKey,
+ },
+ {
+ onConnected: succeed,
+ onError: (message) => fail(message.message),
+ },
+ )
+ }
+
+ function complete(code?: string) {
+ const index = state.methodIndex
+ if (index === undefined) return
+
+ setState({
+ ...state,
+ phase: "connecting",
+ error: undefined,
+ failed: undefined,
+ })
+ action.send(
+ {
+ type: "completeProviderOAuth",
+ providerID: props.providerID,
+ method: index,
+ code,
+ },
+ {
+ onConnected: succeed,
+ onError: (message) => fail(message.message),
+ },
+ )
+ }
+
+ const title = () => language.t("provider.connect.title", { provider: name() })
+
+ const MethodSelection: Component = () => {
+ return (
+
+
{language.t("provider.connect.selectMethod", { provider: name() })}
+
+
+ {(item, index) => (
+
+ )}
+
+
+
+
+
+
+ )
+ }
+
+ const ApiView: Component = () => {
+ const [value, setValue] = createSignal("")
+
+ function submit(e: SubmitEvent) {
+ e.preventDefault()
+ const apiKey = value().trim()
+ if (!apiKey) {
+ setState({ ...state, error: language.t("provider.connect.apiKey.required") })
+ return
+ }
+ connect(apiKey)
+ }
+
+ return (
+
+ )
+ }
+
+ const OAuthCodeView: Component = () => {
+ const [value, setValue] = createSignal("")
+
+ onMount(() => {
+ if (!state.authorization?.url) return
+ openExternal(state.authorization.url)
+ })
+
+ function submit(e: SubmitEvent) {
+ e.preventDefault()
+ const code = value().trim()
+ if (!code) {
+ setState({ ...state, error: language.t("provider.connect.oauth.code.required") })
+ return
+ }
+ complete(code)
+ }
+
+ return (
+
+ )
+ }
+
+ const OAuthAutoView: Component = () => {
+ const code = createMemo(() => {
+ const instructions = state.authorization?.instructions
+ if (!instructions) return ""
+ if (!instructions.includes(":")) return instructions
+ return instructions.split(":")[1]?.trim() ?? instructions
+ })
+
+ onMount(() => {
+ if (state.authorization?.url) openExternal(state.authorization.url)
+ complete()
+ })
+
+ return (
+
+
+
+
+
{language.t("provider.connect.oauth.auto.confirmationCode")}
+
{code()}
+
+
+
+
+
+ {state.error
+ ? language.t("provider.connect.status.failed", { error: state.error })
+ : language.t("provider.connect.status.waiting")}
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+export default ProviderConnectDialog
diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ProviderSelectDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ProviderSelectDialog.tsx
new file mode 100644
index 0000000000..e9085add8d
--- /dev/null
+++ b/packages/kilo-vscode/webview-ui/src/components/settings/ProviderSelectDialog.tsx
@@ -0,0 +1,134 @@
+import { useDialog } from "@kilocode/kilo-ui/context/dialog"
+import { Dialog } from "@kilocode/kilo-ui/dialog"
+import { List } from "@kilocode/kilo-ui/list"
+import { ProviderIcon } from "@kilocode/kilo-ui/provider-icon"
+import { Tag } from "@kilocode/kilo-ui/tag"
+import { Show, createMemo } from "solid-js"
+import { useConfig } from "../../context/config"
+import { useLanguage } from "../../context/language"
+import { useProvider } from "../../context/provider"
+import { useServer } from "../../context/server"
+import type { Provider } from "../../types/messages"
+import ProviderConnectDialog from "./ProviderConnectDialog"
+import {
+ CUSTOM_PROVIDER_ID,
+ isPopularProvider,
+ kiloFallbackProvider,
+ popularProviderIndex,
+ providerIcon,
+} from "./provider-catalog"
+import CustomProviderDialog from "./CustomProviderDialog"
+import { KILO_PROVIDER_ID } from "../../../../src/shared/provider-model"
+
+type ProviderItem = {
+ id: string
+ name: string
+}
+
+const ProviderSelectDialog = () => {
+ const dialog = useDialog()
+ const { config } = useConfig()
+ const provider = useProvider()
+ const server = useServer()
+ const language = useLanguage()
+
+ const items = createMemo(() => {
+ language.locale()
+
+ const disabled = new Set(config().disabled_providers ?? [])
+ const connected = new Set(provider.connected())
+ const all = Object.values(provider.providers())
+ const withKilo = all.some((item) => item.id === KILO_PROVIDER_ID) ? all : [kiloFallbackProvider(), ...all]
+ const available = withKilo.filter((item) => !disabled.has(item.id) && !connected.has(item.id))
+
+ return [
+ {
+ id: CUSTOM_PROVIDER_ID,
+ name: language.t("settings.providers.tag.customProvider"),
+ },
+ ...available.map((item) => ({
+ id: item.id,
+ name: item.name,
+ })),
+ ]
+ })
+
+ function open(item: ProviderItem) {
+ if (item.id === CUSTOM_PROVIDER_ID) {
+ dialog.show(() => dialog.show(() => )} />)
+ return
+ }
+
+ if (item.id === KILO_PROVIDER_ID) {
+ dialog.close()
+ server.startLogin()
+ return
+ }
+
+ dialog.show(() => )
+ }
+
+ return (
+
+ )
+}
+
+export default ProviderSelectDialog
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 cf7f642782..4f4408b773 100644
--- a/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx
+++ b/packages/kilo-vscode/webview-ui/src/components/settings/ProvidersTab.tsx
@@ -1,218 +1,315 @@
-import { Component, For, createSignal, createMemo } from "solid-js"
-import { Select } from "@kilocode/kilo-ui/select"
-import { Card } from "@kilocode/kilo-ui/card"
import { Button } from "@kilocode/kilo-ui/button"
-import { IconButton } from "@kilocode/kilo-ui/icon-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 { Tag } from "@kilocode/kilo-ui/tag"
+import { showToast } from "@kilocode/kilo-ui/toast"
+import { Component, For, Show, createMemo, onCleanup } from "solid-js"
import { useConfig } from "../../context/config"
-import { useProvider } from "../../context/provider"
import { useLanguage } from "../../context/language"
-import { useSession } from "../../context/session"
-import { ModelSelectorBase } from "../shared/ModelSelector"
-import type { ModelSelection } from "../../types/messages"
-import SettingsRow from "./SettingsRow"
+import { useProvider } from "../../context/provider"
+import { useServer } from "../../context/server"
+import { useVSCode } from "../../context/vscode"
+import type { Provider } from "../../types/messages"
+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 { KILO_PROVIDER_ID } from "../../../../src/shared/provider-model"
+import { createProviderAction } from "../../utils/provider-action"
-interface ProviderOption {
- value: string
- label: string
-}
-
-/** Parse a "provider/model" config string into a ModelSelection (or null). */
-function parseModelConfig(raw: string | undefined): ModelSelection | null {
- if (!raw) {
- return null
- }
- const slash = raw.indexOf("/")
- if (slash <= 0) {
- return null
- }
- return { providerID: raw.slice(0, slash), modelID: raw.slice(slash + 1) }
-}
+type ProviderSource = "env" | "api" | "config" | "custom"
const ProvidersTab: Component = () => {
- const { config, updateConfig } = useConfig()
+ const dialog = useDialog()
+ const { config } = useConfig()
const provider = useProvider()
const language = useLanguage()
- const session = useSession()
+ const server = useServer()
+ const vscode = useVSCode()
+ const action = createProviderAction(vscode)
- const providerOptions = createMemo(() =>
- Object.keys(provider.providers())
- .sort()
- .map((id) => ({ value: id, label: id })),
- )
+ onCleanup(action.dispose)
- const [newDisabled, setNewDisabled] = createSignal()
+ const kiloLoggedIn = createMemo(() => !!server.profileData())
- const disabledProviders = () => config().disabled_providers ?? []
+ const connectedProviders = createMemo(() => {
+ const ids = visibleConnectedIds(provider.connected(), provider.authStates())
+ const all = provider.providers()
+ return ids
+ .filter((id) => id !== KILO_PROVIDER_ID)
+ .map((id) => all[id])
+ .filter((item): item is Provider => !!item)
+ })
- const addDisabled = (value: string) => {
- const current = [...disabledProviders()]
- if (value && !current.includes(value)) {
- current.push(value)
- updateConfig({ disabled_providers: current })
- }
+ const popularProviders = createMemo(() => {
+ const connected = new Set(provider.connected())
+ const disabled = new Set(config().disabled_providers ?? [])
+ const all = Object.values(provider.providers())
+ return sortProviders(
+ all.filter(
+ (item) =>
+ item.id !== KILO_PROVIDER_ID &&
+ isPopularProvider(item.id) &&
+ !connected.has(item.id) &&
+ !disabled.has(item.id),
+ ),
+ )
+ })
+
+ function source(item: Provider): ProviderSource | undefined {
+ if (!("source" in item)) return
+ const value = (item as Provider & { source?: string }).source
+ if (value === "env" || value === "api" || value === "config" || value === "custom") return value
+ return
}
- const removeDisabled = (index: number) => {
- const current = [...disabledProviders()]
- current.splice(index, 1)
- updateConfig({ disabled_providers: current })
+ function sourceTag(item: Provider) {
+ const current = source(item)
+ if (current === "env") return language.t("settings.providers.tag.environment")
+ if (current === "api") return language.t("provider.connect.method.apiKey")
+ if (current === "config") {
+ const cfg = config().provider?.[item.id]
+ if (cfg?.npm === "@ai-sdk/openai-compatible") return language.t("settings.providers.tag.custom")
+ return language.t("settings.providers.tag.config")
+ }
+ if (current === "custom") return language.t("settings.providers.tag.custom")
+ return language.t("settings.providers.tag.other")
}
- function handleModelSelect(configKey: "model" | "small_model") {
- return (providerID: string, modelID: string) => {
- if (!providerID || !modelID) {
- updateConfig({ [configKey]: null })
- } else {
- updateConfig({ [configKey]: `${providerID}/${modelID}` })
- }
- }
+ function canDisconnect(item: Provider) {
+ return source(item) !== "env"
}
- const allAgents = createMemo(() => session.agents())
+ function disconnect(providerID: string, name: string) {
+ action.send(
+ { type: "disconnectProvider", providerID },
+ {
+ onDisconnected: () => {
+ showToast({
+ variant: "success",
+ icon: "circle-check",
+ title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
+ description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
+ })
+ },
+ onError: (message) => {
+ showToast({ title: language.t("common.requestFailed"), description: message.message })
+ },
+ },
+ )
+ }
- function handleModeModelSelect(agentName: string) {
- return (providerID: string, modelID: string) => {
- if (!providerID || !modelID) {
- updateConfig({ agent: { [agentName]: { model: null } } })
- } else {
- updateConfig({ agent: { [agentName]: { model: `${providerID}/${modelID}` } } })
- }
+ function connectProvider(item: Provider) {
+ if (item.id === KILO_PROVIDER_ID) {
+ server.startLogin()
+ return
}
+ dialog.show(() => )
}
return (
- {/* Model selection */}
+ {/* Kilo Gateway — always at the top, not editable */}
-
-
-
-
-
-
-
-
- {/* Model per Mode */}
-
{language.t("settings.providers.modeModels")}
-
-
- {(agent, index) => (
-
-
-
- )}
-
-
-
- {/* Beta notice */}
-
-
- {language.t("settings.providers.betaNotice")}
-
-
- {/* Disabled providers */}
-
{language.t("settings.providers.disabled")}
-
-
- {language.t("settings.providers.disabled.description")}
-
0 ? "1px solid var(--border-weak-base)" : "none",
+ gap: "12px",
+ "min-height": "56px",
+ padding: "12px 0",
}}
>
-
-
-
+ }
>
- {language.t("common.add")}
-
+
{language.t("settings.providers.tag.gateway")}
+
-
- {(id, index) => (
+
+
+ {/* Connected providers (excluding Kilo) */}
+
+ {language.t("settings.providers.section.connected")}
+
+
+ 0}
+ fallback={
- {id}
- removeDisabled(index())} />
+ {language.t("settings.providers.connected.empty")}
- )}
-
+ }
+ >
+
+ {(item) => (
+
+
+
+
+ {item.name}
+
+
{sourceTag(item)}
+
+
+ {language.t("settings.providers.connected.environmentDescription")}
+
+ }
+ >
+
+
+
+ )}
+
+
+
+ {/* Popular providers */}
+
+ {language.t("settings.providers.section.popular")}
+
+
+
+ {(item) => {
+ const noteKey = providerNoteKey(item.id)
+ return (
+
+
+
+
+ {(key) => (
+
+ {language.t(key())}
+
+ )}
+
+
+
+
+ )
+ }}
+
+
+ {/* Custom provider entry */}
+
+
+
+
+
+ {language.t("provider.custom.title")}
+
+
{language.t("settings.providers.tag.custom")}
+
+
+ {language.t("settings.providers.custom.description")}
+
+
+
+
+
+
+ {/* View all providers link */}
+
+
+
)
}
diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx
index 40f55833c7..f632907321 100644
--- a/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx
+++ b/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx
@@ -7,6 +7,7 @@ import { useVSCode } from "../../context/vscode"
import { useLanguage } from "../../context/language"
import { useConfig } from "../../context/config"
import { useSession } from "../../context/session"
+import ModelsTab from "./ModelsTab"
import ProvidersTab from "./ProvidersTab"
import AgentBehaviourTab from "./AgentBehaviourTab"
import AutoApproveTab from "./AutoApproveTab"
@@ -97,6 +98,10 @@ const Settings: Component = (props) => {
style={{ flex: 1, overflow: "hidden" }}
>
+
+
+ {language.t("settings.models.title")}
+
{language.t("settings.providers.title")}
@@ -155,6 +160,10 @@ const Settings: Component = (props) => {
+
+ {language.t("settings.models.title")}
+
+
{language.t("settings.providers.title")}
diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/provider-catalog.ts b/packages/kilo-vscode/webview-ui/src/components/settings/provider-catalog.ts
new file mode 100644
index 0000000000..8594dc3d8e
--- /dev/null
+++ b/packages/kilo-vscode/webview-ui/src/components/settings/provider-catalog.ts
@@ -0,0 +1,51 @@
+import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
+import type { Provider } from "../../types/messages"
+import {
+ KILO_PROVIDER_ID,
+ PROVIDER_PRIORITY as POPULAR_PROVIDER_IDS,
+ createKiloFallbackProvider,
+ providerOrderIndex,
+} from "../../../../src/shared/provider-model"
+
+export const CUSTOM_PROVIDER_ID = "_custom"
+export { POPULAR_PROVIDER_IDS }
+
+const POPULAR_PROVIDER_SET = new Set(POPULAR_PROVIDER_IDS)
+
+export function isPopularProvider(providerID: string) {
+ return POPULAR_PROVIDER_SET.has(providerID)
+}
+
+export function popularProviderIndex(providerID: string) {
+ return providerOrderIndex(providerID, POPULAR_PROVIDER_IDS)
+}
+
+export function providerIcon(providerID: string): IconName {
+ if (providerID === KILO_PROVIDER_ID) return "synthetic"
+ if (iconNames.includes(providerID as IconName)) return providerID as IconName
+ return "synthetic"
+}
+
+export function kiloFallbackProvider(): Provider {
+ return createKiloFallbackProvider()
+}
+
+export function providerNoteKey(providerID: string) {
+ if (providerID === "kilo") return "dialog.provider.kilo.note"
+ if (providerID === "opencode") return "dialog.provider.opencode.note"
+ if (providerID === "anthropic") return "dialog.provider.anthropic.note"
+ if (providerID.startsWith("github-copilot")) return "dialog.provider.copilot.note"
+ if (providerID === "openai") return "dialog.provider.openai.note"
+ if (providerID === "google") return "dialog.provider.google.note"
+ if (providerID === "openrouter") return "dialog.provider.openrouter.note"
+ if (providerID === "vercel") return "dialog.provider.vercel.note"
+ return undefined
+}
+
+export function sortProviders(items: Provider[]) {
+ return items.slice().sort((a, b) => {
+ const rank = popularProviderIndex(a.id) - popularProviderIndex(b.id)
+ if (rank !== 0) return rank
+ return a.name.localeCompare(b.name)
+ })
+}
diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/provider-visibility.ts b/packages/kilo-vscode/webview-ui/src/components/settings/provider-visibility.ts
new file mode 100644
index 0000000000..7832d52311
--- /dev/null
+++ b/packages/kilo-vscode/webview-ui/src/components/settings/provider-visibility.ts
@@ -0,0 +1,6 @@
+import type { ProviderAuthState } from "../../types/messages"
+import { KILO_PROVIDER_ID } from "../../../../src/shared/provider-model"
+
+export function visibleConnectedIds(connected: string[], authStates: Record) {
+ return connected.filter((id) => id !== KILO_PROVIDER_ID || authStates[KILO_PROVIDER_ID] !== undefined)
+}
diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx
index 5268103cd9..cbd158963c 100644
--- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx
+++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx
@@ -65,6 +65,7 @@ export const ModelSelectorBase: Component = (props) => {
})
const hasProviders = () => visibleModels().length > 0
+ const canOpen = () => hasProviders() || ((props.allowClear ?? false) && !!props.value)
// Debounce search input to avoid re-filtering on every keystroke
createEffect(() => {
@@ -217,6 +218,7 @@ export const ModelSelectorBase: Component = (props) => {
buildTriggerLabel(
activeModel()?.name,
activeModel()?.providerID,
+ activeModel()?.providerName,
props.value,
props.allowClear ?? false,
props.clearLabel ?? "",
@@ -237,7 +239,7 @@ export const ModelSelectorBase: Component = (props) => {
triggerProps={{
variant: "secondary",
size: "normal",
- disabled: !hasProviders(),
+ disabled: !canOpen(),
title: activeModel()?.id,
}}
trigger={
diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts b/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts
index 0da86e58d8..195604b73f 100644
--- a/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts
+++ b/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts
@@ -1,18 +1,21 @@
import type { ModelSelection } from "../../types/messages"
import type { EnrichedModel } from "../../context/provider"
+import {
+ KILO_PROVIDER_ID as KILO_GATEWAY_ID,
+ PROVIDER_PRIORITY as PROVIDER_ORDER,
+ providerOrderIndex,
+} from "../../../../src/shared/provider-model"
+
+export { KILO_GATEWAY_ID, PROVIDER_ORDER }
-export const KILO_GATEWAY_ID = "kilo"
export const KILO_AUTO_SMALL_IDS = new Set(["kilo-auto/small", "auto-small"])
export function isSmall(model: Pick): boolean {
return model.providerID === KILO_GATEWAY_ID && KILO_AUTO_SMALL_IDS.has(model.id)
}
-export const PROVIDER_ORDER = [KILO_GATEWAY_ID, "anthropic", "openai", "google"]
-
-export function providerSortKey(providerID: string, order = PROVIDER_ORDER): number {
- const idx = order.indexOf(providerID.toLowerCase())
- return idx >= 0 ? idx : order.length
+export function providerSortKey(providerID: string, order: readonly string[] = PROVIDER_ORDER): number {
+ return providerOrderIndex(providerID, order as typeof PROVIDER_ORDER)
}
export function isFree(model: Pick): boolean {
@@ -30,13 +33,18 @@ export function stripSubProviderPrefix(name: string): string {
export function buildTriggerLabel(
resolvedName: string | undefined,
providerID: string | undefined,
+ providerName: string | undefined,
raw: ModelSelection | null,
allowClear: boolean,
clearLabel: string,
hasProviders: boolean,
labels: { select: string; noProviders: string; notSet: string },
): string {
- if (resolvedName) return providerID === KILO_GATEWAY_ID ? stripSubProviderPrefix(resolvedName) : resolvedName
+ if (resolvedName) {
+ if (providerID === KILO_GATEWAY_ID) return stripSubProviderPrefix(resolvedName)
+ if (providerName) return `${providerName} / ${resolvedName}`
+ return resolvedName
+ }
if (raw?.providerID && raw?.modelID) {
return raw.providerID === KILO_GATEWAY_ID ? raw.modelID : `${raw.providerID} / ${raw.modelID}`
}
diff --git a/packages/kilo-vscode/webview-ui/src/context/model-selection.ts b/packages/kilo-vscode/webview-ui/src/context/model-selection.ts
new file mode 100644
index 0000000000..86d9a255b9
--- /dev/null
+++ b/packages/kilo-vscode/webview-ui/src/context/model-selection.ts
@@ -0,0 +1,43 @@
+import type { ModelSelection, Provider } from "../types/messages"
+import { isModelValid } from "./provider-utils"
+
+function validate(
+ providers: Record,
+ connected: string[],
+ selection: ModelSelection | null | undefined,
+): ModelSelection | null {
+ if (!selection) return null
+ if (Object.keys(providers).length === 0) return selection
+ return isModelValid(providers, connected, selection) ? selection : null
+}
+
+function recent(
+ providers: Record,
+ connected: string[],
+ selections: ModelSelection[] | undefined,
+): ModelSelection | null {
+ for (const item of selections ?? []) {
+ const selection = validate(providers, connected, item)
+ if (selection) return selection
+ }
+ return null
+}
+
+export function resolveModelSelection(input: {
+ providers: Record
+ connected: string[]
+ override?: ModelSelection | null
+ mode?: ModelSelection | null
+ global?: ModelSelection | null
+ recent?: ModelSelection[]
+ fallback?: ModelSelection | null
+}): ModelSelection | null {
+ return (
+ validate(input.providers, input.connected, input.override) ??
+ validate(input.providers, input.connected, input.mode) ??
+ validate(input.providers, input.connected, input.global) ??
+ recent(input.providers, input.connected, input.recent) ??
+ input.fallback ??
+ null
+ )
+}
diff --git a/packages/kilo-vscode/webview-ui/src/context/provider-utils.ts b/packages/kilo-vscode/webview-ui/src/context/provider-utils.ts
index 0121b09d67..6afa2f6b5e 100644
--- a/packages/kilo-vscode/webview-ui/src/context/provider-utils.ts
+++ b/packages/kilo-vscode/webview-ui/src/context/provider-utils.ts
@@ -28,3 +28,19 @@ export function findModel(models: EnrichedModel[], selection: ModelSelection | n
if (!selection) return undefined
return models.find((m) => m.providerID === selection.providerID && m.id === selection.modelID)
}
+
+/**
+ * True when the selection points to an existing model in a connected provider.
+ * Kilo gateway models remain usable whenever the provider catalog exposes them.
+ */
+export function isModelValid(
+ providers: Record,
+ connected: string[],
+ selection: ModelSelection | null,
+): boolean {
+ if (!selection) return false
+ const provider = providers[selection.providerID]
+ if (!provider) return false
+ if (selection.providerID !== "kilo" && !connected.includes(selection.providerID)) return false
+ return !!provider.models[selection.modelID]
+}
diff --git a/packages/kilo-vscode/webview-ui/src/context/provider.tsx b/packages/kilo-vscode/webview-ui/src/context/provider.tsx
index 358f5d7084..f5f95d2d91 100644
--- a/packages/kilo-vscode/webview-ui/src/context/provider.tsx
+++ b/packages/kilo-vscode/webview-ui/src/context/provider.tsx
@@ -6,8 +6,10 @@
import { createContext, useContext, createSignal, createMemo, onCleanup, ParentComponent, Accessor } from "solid-js"
import { useVSCode } from "./vscode"
-import type { Provider, ProviderModel, ModelSelection, ExtensionMessage } from "../types/messages"
-import { flattenModels, findModel as _findModel } from "./provider-utils"
+import type { Provider, ProviderModel, ModelSelection, ExtensionMessage, ProviderAuthState } from "../types/messages"
+import type { ProviderAuthMethod } from "@kilocode/sdk/v2/client"
+import { flattenModels, findModel as _findModel, isModelValid as isValid } from "./provider-utils"
+import { KILO_AUTO } from "../../../src/shared/provider-model"
export type EnrichedModel = ProviderModel & { providerID: string; providerName: string }
@@ -18,10 +20,11 @@ interface ProviderContextValue {
defaultSelection: Accessor
models: Accessor
findModel: (selection: ModelSelection | null) => EnrichedModel | undefined
+ authMethods: Accessor>
+ authStates: Accessor>
+ isModelValid: (selection: ModelSelection | null) => boolean
}
-const KILO_AUTO: ModelSelection = { providerID: "kilo", modelID: "kilo-auto/free" }
-
export const ProviderContext = createContext()
export const ProviderProvider: ParentComponent = (props) => {
@@ -31,6 +34,8 @@ export const ProviderProvider: ParentComponent = (props) => {
const [connected, setConnected] = createSignal([])
const [defaults, setDefaults] = createSignal>({})
const [defaultSelection, setDefaultSelection] = createSignal(KILO_AUTO)
+ const [authMethods, setAuthMethods] = createSignal>({})
+ const [authStates, setAuthStates] = createSignal>({})
const models = createMemo(() => flattenModels(providers()))
@@ -38,6 +43,10 @@ export const ProviderProvider: ParentComponent = (props) => {
return _findModel(models(), selection)
}
+ function isModelValid(selection: ModelSelection | null): boolean {
+ return isValid(providers(), connected(), selection)
+ }
+
// Register handler immediately (not in onMount) so we never miss
// a providersLoaded message that arrives before the DOM mount.
const unsubscribe = vscode.onMessage((message: ExtensionMessage) => {
@@ -49,6 +58,8 @@ export const ProviderProvider: ParentComponent = (props) => {
setConnected(message.connected)
setDefaults(message.defaults)
setDefaultSelection(message.defaultSelection)
+ setAuthMethods(message.authMethods)
+ setAuthStates(message.authStates)
})
onCleanup(unsubscribe)
@@ -80,6 +91,9 @@ export const ProviderProvider: ParentComponent = (props) => {
defaultSelection,
models,
findModel,
+ authMethods,
+ authStates,
+ isModelValid,
}
return {props.children}
diff --git a/packages/kilo-vscode/webview-ui/src/context/server.tsx b/packages/kilo-vscode/webview-ui/src/context/server.tsx
index 1688d49cea..7c293d32ed 100644
--- a/packages/kilo-vscode/webview-ui/src/context/server.tsx
+++ b/packages/kilo-vscode/webview-ui/src/context/server.tsx
@@ -126,6 +126,10 @@ export const ServerProvider: ParentComponent = (props) => {
})
const startLogin = () => {
+ const status = deviceAuth().status
+ if (status === "initiating" || status === "pending") {
+ return
+ }
setDeviceAuth({ status: "initiating" })
vscode.postMessage({ type: "login" })
}
diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx
index d09f3b55cc..f8a4f43d50 100644
--- a/packages/kilo-vscode/webview-ui/src/context/session.tsx
+++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx
@@ -44,6 +44,10 @@ import type {
import { removeSessionPermissions, upsertPermission } from "./permission-queue"
import { computeStatus, calcTotalCost, calcContextUsage } from "./session-utils"
import { Identifier } from "../utils/id"
+import { resolveModelSelection } from "./model-selection"
+import { KILO_AUTO, parseModelString } from "../../../src/shared/provider-model"
+
+const RECENT_LIMIT = 5
// Store structure for messages and parts
interface SessionStore {
@@ -51,10 +55,11 @@ interface SessionStore {
messages: Record // sessionID -> messages
parts: Record // messageID -> parts
todos: Record // sessionID -> todos
- modelSelections: Record // agentName -> model (global, extension-lifetime)
+ modelSelections: Record // agentName -> model (global, extension-lifetime)
sessionOverrides: Record // sessionID -> per-session model override (compare mode)
agentSelections: Record // sessionID -> agent name
variantSelections: Record // "providerID/modelID" -> variant name
+ recentModels: ModelSelection[]
}
interface SessionContextValue {
@@ -272,6 +277,7 @@ export const SessionProvider: ParentComponent = (props) => {
sessionOverrides: {},
agentSelections: {},
variantSelections: {},
+ recentModels: [],
})
// Per-session agent selection
@@ -283,34 +289,35 @@ export const SessionProvider: ParentComponent = (props) => {
return pendingAgentSelection() ?? defaultAgent()
})
- /** Parse a "provider/model" config string into a ModelSelection (or null). */
- function parseModel(raw: string | undefined | null): ModelSelection | null {
- if (!raw) return null
- const slash = raw.indexOf("/")
- if (slash <= 0) return null
- return { providerID: raw.slice(0, slash), modelID: raw.slice(slash + 1) }
- }
-
/** Per-mode model from config (e.g. config.agent.code.model). */
function getModeModel(agentName: string): ModelSelection | null {
- return parseModel(config().agent?.[agentName]?.model)
+ return parseModelString(config().agent?.[agentName]?.model)
}
/** Global default model from config (config.model). */
function getGlobalModel(): ModelSelection | null {
- return parseModel(config().model)
+ return parseModelString(config().model)
+ }
+
+ function resolveModel(agentName: string, override?: ModelSelection | null): ModelSelection | null {
+ return resolveModelSelection({
+ providers: provider.providers(),
+ connected: provider.connected(),
+ override,
+ mode: getModeModel(agentName),
+ global: getGlobalModel(),
+ recent: store.recentModels,
+ fallback: KILO_AUTO,
+ })
}
// Keep model selection in sync with provider/mode default until the user
// explicitly overrides it.
createEffect(() => {
- const def = provider.defaultSelection()
const agentName = selectedAgentName()
if (userSetAgents()[agentName]) return
-
- // Per-mode config > global config model > VS Code default selection
- const sel = getModeModel(agentName) ?? getGlobalModel() ?? def
- if (sel) setStore("modelSelections", agentName, sel)
+ const sel = resolveModel(agentName)
+ setStore("modelSelections", agentName, sel)
})
// Global model selection per agent/mode
@@ -322,23 +329,33 @@ export const SessionProvider: ParentComponent = (props) => {
if (session) return session
}
const agentName = selectedAgentName()
- const override = store.modelSelections[agentName]
- if (override) return override
- return getModeModel(agentName) ?? getGlobalModel() ?? provider.defaultSelection()
+ return resolveModel(agentName, store.modelSelections[agentName])
})
- function selectModel(providerID: string, modelID: string) {
- const agentName = selectedAgentName()
+
+ function pushRecent(selection: ModelSelection) {
+ const key = `${selection.providerID}/${selection.modelID}`
+ const filtered = store.recentModels.filter((r) => `${r.providerID}/${r.modelID}` !== key)
+ const updated = [selection, ...filtered].slice(0, RECENT_LIMIT)
+ setStore("recentModels", updated)
+ vscode.postMessage({ type: "persistRecents", recents: updated })
+ }
+
+ function applyModel(agentName: string, selection: ModelSelection) {
setUserSetAgents((prev) => ({ ...prev, [agentName]: true }))
- setStore("modelSelections", agentName, { providerID, modelID })
- // Update per-session override so compare-mode sessions stay independent
+ setStore("modelSelections", agentName, selection)
+ pushRecent(selection)
const sid = currentSessionID()
- if (sid) setStore("sessionOverrides", sid, { providerID, modelID })
+ if (sid) setStore("sessionOverrides", sid, selection)
+ }
+
+ function selectModel(providerID: string, modelID: string) {
+ applyModel(selectedAgentName(), { providerID, modelID })
}
/** The config/default model for the current mode (what settings says). */
const configModel = createMemo(() => {
const agentName = selectedAgentName()
- return getModeModel(agentName) ?? getGlobalModel() ?? provider.defaultSelection()
+ return resolveModel(agentName)
})
/** True when the active model differs from what the config dictates. */
@@ -470,6 +487,14 @@ export const SessionProvider: ParentComponent = (props) => {
onCleanup(unsubVariants)
+ // Load persisted recent models from extension globalState
+ const unsubRecents = vscode.onMessage((message: ExtensionMessage) => {
+ if (message.type !== "recentsLoaded") return
+ setStore("recentModels", message.recents)
+ })
+ vscode.postMessage({ type: "requestRecents" })
+ onCleanup(unsubRecents)
+
// Handle messages from extension
onMount(() => {
const unsubscribe = vscode.onMessage((message: ExtensionMessage) => {
@@ -1113,9 +1138,7 @@ export const SessionProvider: ParentComponent = (props) => {
// When switching mode, initialize model for the new mode if the user
// hasn't explicitly set one for it
if (!userSetAgents()[name] && !store.modelSelections[name]) {
- const modeModel = getModeModel(name)
- const sel = modeModel ?? provider.defaultSelection()
- if (sel) setStore("modelSelections", name, sel)
+ setStore("modelSelections", name, resolveModel(name))
}
}
}
@@ -1559,7 +1582,7 @@ export const SessionProvider: ParentComponent = (props) => {
const override = store.sessionOverrides[sessionID]
if (override) return override
const agentName = store.agentSelections[sessionID] ?? defaultAgent()
- return store.modelSelections[agentName] ?? provider.defaultSelection()
+ return resolveModel(agentName, store.modelSelections[agentName])
},
setSessionModel: (sessionID: string, providerID: string, modelID: string) => {
// Only write per-session override — do NOT touch global modelSelections or
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts
index 8414cc3bd6..f318490e34 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts
@@ -551,6 +551,8 @@ export const dict = {
"terminal.connectionLost.description": "انقطع اتصال المحطة الطرفية. يمكن أن يحدث هذا عند إعادة تشغيل الخادم.",
"common.closeTab": "إغلاق علامة التبويب",
+ "common.signIn": "تسجيل الدخول",
+ "common.signOut": "تسجيل الخروج",
"common.dismiss": "رفض",
"common.requestFailed": "فشل الطلب",
"common.moreOptions": "مزيد من الخيارات",
@@ -704,13 +706,56 @@ export const dict = {
"settings.providers.section.connected": "الموفرون المتصلون",
"settings.providers.connected.empty": "لا يوجد موفرون متصلون",
"settings.providers.section.popular": "الموفرون الشائعون",
+ "settings.providers.search.placeholder": "البحث عن موفرين",
+ "settings.providers.select.placeholder": "اختر موفرًا...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "البيئة",
"settings.providers.tag.config": "التكوين",
"settings.providers.tag.custom": "مخصص",
"settings.providers.tag.other": "أخرى",
+ "settings.providers.tag.customProvider": "مزود مخصص",
+ "settings.providers.connected.environmentDescription": "متصل من متغيرات البيئة الخاصة بك",
+ "settings.providers.custom.description": "أضف مزوداً متوافقاً مع OpenAI عبر عنوان URL الأساسي.",
"settings.providers.modeModels": "نموذج لكل وضع",
+ "settings.providers.custom.note": "أضف موفرًا متوافقًا مع OpenAI عبر عنوان URL الأساسي.",
"settings.providers.modeModels.description":
"تجاوز النموذج الافتراضي لأوضاع محددة. إذا لم يتم التعيين، يتم استخدام النموذج الافتراضي العام.",
+ "provider.custom.title": "مزود مخصص",
+ "provider.custom.description.prefix": "قم بتكوين مزود متوافق مع OpenAI. انظر ",
+ "provider.custom.description.link": "وثائق تكوين المزود",
+ "provider.custom.description.suffix": ".",
+ "provider.custom.field.providerID.label": "معرف المزود",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "أحرف صغيرة وأرقام وواصلات أو شرطات سفلية",
+ "provider.custom.field.name.label": "الاسم المعروض",
+ "provider.custom.field.name.placeholder": "مزود الذكاء الاصطناعي",
+ "provider.custom.field.baseURL.label": "عنوان URL الأساسي",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "مفتاح API",
+ "provider.custom.field.apiKey.placeholder": "مفتاح API",
+ "provider.custom.field.apiKey.description": "اختياري. اتركه فارغاً إذا كنت تدير المصادقة عبر الرؤوس.",
+ "provider.custom.models.label": "النماذج",
+ "provider.custom.models.id.label": "المعرف",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "الاسم",
+ "provider.custom.models.name.placeholder": "الاسم المعروض",
+ "provider.custom.models.remove": "إزالة النموذج",
+ "provider.custom.models.add": "إضافة نموذج",
+ "provider.custom.headers.label": "الرؤوس (اختياري)",
+ "provider.custom.headers.key.label": "الرأس",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "القيمة",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "إزالة الرأس",
+ "provider.custom.headers.add": "إضافة رأس",
+ "provider.custom.error.providerID.required": "معرف المزود مطلوب",
+ "provider.custom.error.providerID.format": "استخدم أحرفاً صغيرة وأرقاماً وواصلات أو شرطات سفلية",
+ "provider.custom.error.providerID.exists": "معرف المزود هذا موجود بالفعل",
+ "provider.custom.error.name.required": "الاسم المعروض مطلوب",
+ "provider.custom.error.baseURL.required": "عنوان URL الأساسي مطلوب",
+ "provider.custom.error.baseURL.format": "يجب أن يبدأ بـ http:// أو https://",
+ "provider.custom.error.required": "مطلوب",
+ "provider.custom.error.duplicate": "مكرر",
"settings.models.title": "النماذج",
"settings.models.description": "ستكون إعدادات النموذج قابلة للتكوين هنا.",
"settings.agents.title": "الوكلاء",
@@ -813,6 +858,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "تم نسخ الرابط إلى الحافظة",
"deviceAuth.toast.codeCopied": "تم نسخ الرمز إلى الحافظة",
+ "deviceAuth.toast.errorCopied": "تم نسخ الخطأ إلى الحافظة",
"deviceAuth.status.initiating": "جارٍ بدء تسجيل الدخول...",
"deviceAuth.title": "تسجيل الدخول إلى Kilo Code",
"deviceAuth.step1": "الخطوة 1: افتح هذا الرابط",
@@ -824,8 +870,11 @@ export const dict = {
"deviceAuth.status.waiting": "في انتظار التفويض...",
"deviceAuth.status.success": "تم تسجيل الدخول بنجاح!",
"deviceAuth.status.failed": "فشل تسجيل الدخول",
+ "deviceAuth.error.detailsTitle": "تفاصيل خطأ تسجيل الدخول",
"deviceAuth.status.cancelled": "تم إلغاء تسجيل الدخول",
"deviceAuth.action.tryAgain": "حاول مرة أخرى",
+ "deviceAuth.action.copyError": "نسخ الخطأ",
+ "deviceAuth.action.showDetails": "عرض التفاصيل",
"common.retry": "إعادة المحاولة",
"common.refresh": "تحديث",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts
index ba6db101d0..712435d326 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts
@@ -556,6 +556,8 @@ export const dict = {
"A conexão do terminal foi interrompida. Isso pode acontecer quando o servidor reinicia.",
"common.closeTab": "Fechar aba",
+ "common.signIn": "Entrar",
+ "common.signOut": "Sair",
"common.dismiss": "Descartar",
"common.requestFailed": "Requisição falhou",
"common.moreOptions": "Mais opções",
@@ -711,13 +713,56 @@ export const dict = {
"settings.providers.section.connected": "Provedores conectados",
"settings.providers.connected.empty": "Nenhum provedor conectado",
"settings.providers.section.popular": "Provedores populares",
+ "settings.providers.search.placeholder": "Buscar provedores",
+ "settings.providers.select.placeholder": "Selecionar provedor...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Ambiente",
"settings.providers.tag.config": "Configuração",
"settings.providers.tag.custom": "Personalizado",
"settings.providers.tag.other": "Outro",
+ "settings.providers.tag.customProvider": "Provedor personalizado",
+ "settings.providers.connected.environmentDescription": "Conectado a partir das suas variáveis de ambiente",
+ "settings.providers.custom.description": "Adicione um provedor compatível com OpenAI pela URL base.",
"settings.providers.modeModels": "Modelo por Modo",
+ "settings.providers.custom.note": "Adicione um provedor compatível com OpenAI por URL base.",
"settings.providers.modeModels.description":
"Substitua o modelo padrão para modos específicos. Se não definido, o modelo padrão global é usado.",
+ "provider.custom.title": "Provedor personalizado",
+ "provider.custom.description.prefix": "Configure um provedor compatível com OpenAI. Veja a ",
+ "provider.custom.description.link": "documentação de configuração de provedores",
+ "provider.custom.description.suffix": ".",
+ "provider.custom.field.providerID.label": "ID do provedor",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "Letras minúsculas, números, hifens ou underscores",
+ "provider.custom.field.name.label": "Nome de exibição",
+ "provider.custom.field.name.placeholder": "Meu Provedor de IA",
+ "provider.custom.field.baseURL.label": "URL base",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "Chave de API",
+ "provider.custom.field.apiKey.placeholder": "Chave de API",
+ "provider.custom.field.apiKey.description": "Opcional. Deixe vazio se você gerencia autenticação via headers.",
+ "provider.custom.models.label": "Modelos",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "Nome",
+ "provider.custom.models.name.placeholder": "Nome de Exibição",
+ "provider.custom.models.remove": "Remover modelo",
+ "provider.custom.models.add": "Adicionar modelo",
+ "provider.custom.headers.label": "Headers (opcional)",
+ "provider.custom.headers.key.label": "Header",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "Valor",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "Remover header",
+ "provider.custom.headers.add": "Adicionar header",
+ "provider.custom.error.providerID.required": "ID do provedor é obrigatório",
+ "provider.custom.error.providerID.format": "Use letras minúsculas, números, hifens ou underscores",
+ "provider.custom.error.providerID.exists": "Esse ID de provedor já existe",
+ "provider.custom.error.name.required": "Nome de exibição é obrigatório",
+ "provider.custom.error.baseURL.required": "URL base é obrigatória",
+ "provider.custom.error.baseURL.format": "Deve começar com http:// ou https://",
+ "provider.custom.error.required": "Obrigatório",
+ "provider.custom.error.duplicate": "Duplicado",
"settings.models.title": "Modelos",
"settings.models.description": "Configurações de modelos estarão disponíveis aqui.",
"settings.agents.title": "Agentes",
@@ -821,6 +866,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "URL copiada para a área de transferência",
"deviceAuth.toast.codeCopied": "Código copiado para a área de transferência",
+ "deviceAuth.toast.errorCopied": "Erro copiado para a área de transferência",
"deviceAuth.status.initiating": "Iniciando login...",
"deviceAuth.title": "Entrar no Kilo Code",
"deviceAuth.step1": "Passo 1: Abra esta URL",
@@ -832,8 +878,11 @@ export const dict = {
"deviceAuth.status.waiting": "Aguardando autorização...",
"deviceAuth.status.success": "Login realizado com sucesso!",
"deviceAuth.status.failed": "Falha no login",
+ "deviceAuth.error.detailsTitle": "Detalhes do erro de login",
"deviceAuth.status.cancelled": "Login cancelado",
"deviceAuth.action.tryAgain": "Tentar Novamente",
+ "deviceAuth.action.copyError": "Copiar erro",
+ "deviceAuth.action.showDetails": "Ver detalhes",
"common.retry": "Tentar novamente",
"common.refresh": "Atualizar",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts
index fbfd240309..f7698a5e97 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts
@@ -560,6 +560,8 @@ export const dict = {
"Veza s terminalom je prekinuta. Ovo se može desiti kada se server restartuje.",
"common.closeTab": "Zatvori karticu",
+ "common.signIn": "Prijava",
+ "common.signOut": "Odjava",
"common.dismiss": "Odbaci",
"common.requestFailed": "Zahtjev nije uspio",
"common.moreOptions": "Više opcija",
@@ -715,13 +717,57 @@ export const dict = {
"settings.providers.section.connected": "Povezani provajderi",
"settings.providers.connected.empty": "Nema povezanih provajdera",
"settings.providers.section.popular": "Popularni provajderi",
+ "settings.providers.search.placeholder": "Pretraži provajdere",
+ "settings.providers.select.placeholder": "Odaberi provajdera...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Okruženje",
"settings.providers.tag.config": "Konfiguracija",
"settings.providers.tag.custom": "Prilagođeno",
"settings.providers.tag.other": "Ostalo",
+ "settings.providers.tag.customProvider": "Prilagođeni provajder",
+ "settings.providers.connected.environmentDescription": "Povezano iz vaših varijabli okruženja",
+ "settings.providers.custom.description": "Dodaj OpenAI-kompatibilan provajder putem osnovnog URL-a.",
"settings.providers.modeModels": "Model po režimu",
+ "settings.providers.custom.note": "Dodajte provajdera kompatibilnog s OpenAI putem osnovnog URL-a.",
"settings.providers.modeModels.description":
"Zamijenite podrazumijevani model za određene režime. Ako nije postavljeno, koristi se globalni podrazumijevani model.",
+ "provider.custom.title": "Prilagođeni provajder",
+ "provider.custom.description.prefix": "Konfiguriši OpenAI-kompatibilan provajder. Pogledaj ",
+ "provider.custom.description.link": "dokumentaciju za konfiguraciju provajdera",
+ "provider.custom.description.suffix": ".",
+ "provider.custom.field.providerID.label": "ID provajdera",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "Mala slova, brojevi, crtice ili donje crte",
+ "provider.custom.field.name.label": "Naziv za prikaz",
+ "provider.custom.field.name.placeholder": "Moj AI provajder",
+ "provider.custom.field.baseURL.label": "Osnovni URL",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "API ključ",
+ "provider.custom.field.apiKey.placeholder": "API ključ",
+ "provider.custom.field.apiKey.description":
+ "Opcionalno. Ostavi prazno ako upravljaš autentifikacijom putem zaglavlja.",
+ "provider.custom.models.label": "Modeli",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "Naziv",
+ "provider.custom.models.name.placeholder": "Naziv za prikaz",
+ "provider.custom.models.remove": "Ukloni model",
+ "provider.custom.models.add": "Dodaj model",
+ "provider.custom.headers.label": "Zaglavlja (opcionalno)",
+ "provider.custom.headers.key.label": "Zaglavlje",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "Vrijednost",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "Ukloni zaglavlje",
+ "provider.custom.headers.add": "Dodaj zaglavlje",
+ "provider.custom.error.providerID.required": "ID provajdera je obavezan",
+ "provider.custom.error.providerID.format": "Koristi mala slova, brojeve, crtice ili donje crte",
+ "provider.custom.error.providerID.exists": "Taj ID provajdera već postoji",
+ "provider.custom.error.name.required": "Naziv za prikaz je obavezan",
+ "provider.custom.error.baseURL.required": "Osnovni URL je obavezan",
+ "provider.custom.error.baseURL.format": "Mora početi sa http:// ili https://",
+ "provider.custom.error.required": "Obavezno",
+ "provider.custom.error.duplicate": "Duplikat",
"settings.models.title": "Modeli",
"settings.models.description": "Postavke modela će se ovdje moći podešavati.",
"settings.agents.title": "Agenti",
@@ -824,6 +870,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "URL kopiran u međuspremnik",
"deviceAuth.toast.codeCopied": "Kod kopiran u međuspremnik",
+ "deviceAuth.toast.errorCopied": "Greška kopirana u međuspremnik",
"deviceAuth.status.initiating": "Pokretanje prijave...",
"deviceAuth.title": "Prijavite se u Kilo Code",
"deviceAuth.step1": "Korak 1: Otvorite ovaj URL",
@@ -835,8 +882,11 @@ export const dict = {
"deviceAuth.status.waiting": "Čekanje na autorizaciju...",
"deviceAuth.status.success": "Prijava uspješna!",
"deviceAuth.status.failed": "Prijava neuspješna",
+ "deviceAuth.error.detailsTitle": "Detalji greške pri prijavi",
"deviceAuth.status.cancelled": "Prijava otkazana",
"deviceAuth.action.tryAgain": "Pokušajte ponovo",
+ "deviceAuth.action.copyError": "Kopiraj grešku",
+ "deviceAuth.action.showDetails": "Prikaži detalje",
"common.retry": "Pokušaj ponovo",
"common.refresh": "Osvježi",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts
index d20a249016..50cf0b1dea 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts
@@ -556,6 +556,8 @@ export const dict = {
"terminal.connectionLost.title": "Forbindelse mistet",
"terminal.connectionLost.description": "Terminalforbindelsen blev afbrudt. Dette kan ske, når serveren genstarter.",
"common.closeTab": "Luk fane",
+ "common.signIn": "Log ind",
+ "common.signOut": "Log ud",
"common.dismiss": "Afvis",
"common.requestFailed": "Forespørgsel mislykkedes",
"common.moreOptions": "Flere muligheder",
@@ -710,13 +712,56 @@ export const dict = {
"settings.providers.section.connected": "Forbundne udbydere",
"settings.providers.connected.empty": "Ingen forbundne udbydere",
"settings.providers.section.popular": "Populære udbydere",
+ "settings.providers.search.placeholder": "Søg udbydere",
+ "settings.providers.select.placeholder": "Vælg udbyder...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Miljø",
"settings.providers.tag.config": "Konfiguration",
"settings.providers.tag.custom": "Brugerdefineret",
"settings.providers.tag.other": "Andet",
+ "settings.providers.tag.customProvider": "Brugerdefineret udbyder",
+ "settings.providers.connected.environmentDescription": "Forbundet fra dine miljøvariabler",
+ "settings.providers.custom.description": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.",
"settings.providers.modeModels": "Model pr. tilstand",
+ "settings.providers.custom.note": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.",
"settings.providers.modeModels.description":
"Tilsidesæt standardmodellen for bestemte tilstande. Hvis ikke angivet, bruges den globale standardmodel.",
+ "provider.custom.title": "Brugerdefineret udbyder",
+ "provider.custom.description.prefix": "Konfigurer en OpenAI-kompatibel udbyder. Se ",
+ "provider.custom.description.link": "dokumentation for udbyderkonfiguration",
+ "provider.custom.description.suffix": ".",
+ "provider.custom.field.providerID.label": "Udbyder-ID",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "Små bogstaver, tal, bindestreger eller understregninger",
+ "provider.custom.field.name.label": "Visningsnavn",
+ "provider.custom.field.name.placeholder": "Min AI-udbyder",
+ "provider.custom.field.baseURL.label": "Basis-URL",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "API-nøgle",
+ "provider.custom.field.apiKey.placeholder": "API-nøgle",
+ "provider.custom.field.apiKey.description": "Valgfrit. Lad stå tom, hvis du administrerer godkendelse via headers.",
+ "provider.custom.models.label": "Modeller",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "Navn",
+ "provider.custom.models.name.placeholder": "Visningsnavn",
+ "provider.custom.models.remove": "Fjern model",
+ "provider.custom.models.add": "Tilføj model",
+ "provider.custom.headers.label": "Headers (valgfrit)",
+ "provider.custom.headers.key.label": "Header",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "Værdi",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "Fjern header",
+ "provider.custom.headers.add": "Tilføj header",
+ "provider.custom.error.providerID.required": "Udbyder-ID er påkrævet",
+ "provider.custom.error.providerID.format": "Brug små bogstaver, tal, bindestreger eller understregninger",
+ "provider.custom.error.providerID.exists": "Det udbyder-ID eksisterer allerede",
+ "provider.custom.error.name.required": "Visningsnavn er påkrævet",
+ "provider.custom.error.baseURL.required": "Basis-URL er påkrævet",
+ "provider.custom.error.baseURL.format": "Skal starte med http:// eller https://",
+ "provider.custom.error.required": "Påkrævet",
+ "provider.custom.error.duplicate": "Duplikat",
"settings.models.title": "Modeller",
"settings.models.description": "Modelindstillinger vil kunne konfigureres her.",
"settings.agents.title": "Agenter",
@@ -819,6 +864,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "URL kopieret til udklipsholder",
"deviceAuth.toast.codeCopied": "Kode kopieret til udklipsholder",
+ "deviceAuth.toast.errorCopied": "Fejl kopieret til udklipsholder",
"deviceAuth.status.initiating": "Starter login...",
"deviceAuth.title": "Log ind på Kilo Code",
"deviceAuth.step1": "Trin 1: Åbn denne URL",
@@ -830,8 +876,11 @@ export const dict = {
"deviceAuth.status.waiting": "Venter på godkendelse...",
"deviceAuth.status.success": "Login vellykket!",
"deviceAuth.status.failed": "Login mislykkedes",
+ "deviceAuth.error.detailsTitle": "Login-fejldetaljer",
"deviceAuth.status.cancelled": "Login annulleret",
"deviceAuth.action.tryAgain": "Prøv igen",
+ "deviceAuth.action.copyError": "Kopiér fejl",
+ "deviceAuth.action.showDetails": "Vis detaljer",
"common.retry": "Prøv igen",
"common.refresh": "Opdatér",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts
index 31428857d4..1b49399337 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts
@@ -564,6 +564,8 @@ export const dict = {
"terminal.connectionLost.description":
"Die Terminalverbindung wurde unterbrochen. Das kann passieren, wenn der Server neu startet.",
"common.closeTab": "Tab schließen",
+ "common.signIn": "Anmelden",
+ "common.signOut": "Abmelden",
"common.dismiss": "Verwerfen",
"common.requestFailed": "Anfrage fehlgeschlagen",
"common.moreOptions": "Weitere Optionen",
@@ -720,13 +722,57 @@ export const dict = {
"settings.providers.section.connected": "Verbundene Anbieter",
"settings.providers.connected.empty": "Keine verbundenen Anbieter",
"settings.providers.section.popular": "Beliebte Anbieter",
+ "settings.providers.search.placeholder": "Anbieter durchsuchen",
+ "settings.providers.select.placeholder": "Anbieter auswählen...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Umgebung",
"settings.providers.tag.config": "Konfiguration",
"settings.providers.tag.custom": "Benutzerdefiniert",
"settings.providers.tag.other": "Andere",
+ "settings.providers.tag.customProvider": "Benutzerdefinierter Anbieter",
+ "settings.providers.connected.environmentDescription": "Verbunden über Ihre Umgebungsvariablen",
+ "settings.providers.custom.description": "Fügen Sie einen OpenAI-kompatiblen Anbieter über die Basis-URL hinzu.",
"settings.providers.modeModels": "Modell pro Modus",
+ "settings.providers.custom.note": "Fügen Sie einen OpenAI-kompatiblen Anbieter per Basis-URL hinzu.",
"settings.providers.modeModels.description":
"Überschreiben Sie das Standardmodell für bestimmte Modi. Wenn nicht festgelegt, wird das globale Standardmodell verwendet.",
+ "provider.custom.title": "Benutzerdefinierter Anbieter",
+ "provider.custom.description.prefix": "Konfigurieren Sie einen OpenAI-kompatiblen Anbieter. Siehe die ",
+ "provider.custom.description.link": "Anbieterkonfigurationsdokumentation",
+ "provider.custom.description.suffix": ".",
+ "provider.custom.field.providerID.label": "Anbieter-ID",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "Kleinbuchstaben, Zahlen, Bindestriche oder Unterstriche",
+ "provider.custom.field.name.label": "Anzeigename",
+ "provider.custom.field.name.placeholder": "Mein KI-Anbieter",
+ "provider.custom.field.baseURL.label": "Basis-URL",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "API-Schlüssel",
+ "provider.custom.field.apiKey.placeholder": "API-Schlüssel",
+ "provider.custom.field.apiKey.description":
+ "Optional. Leer lassen, wenn Sie Authentifizierung über Header verwalten.",
+ "provider.custom.models.label": "Modelle",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "Name",
+ "provider.custom.models.name.placeholder": "Anzeigename",
+ "provider.custom.models.remove": "Modell entfernen",
+ "provider.custom.models.add": "Modell hinzufügen",
+ "provider.custom.headers.label": "Header (optional)",
+ "provider.custom.headers.key.label": "Header",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "Wert",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "Header entfernen",
+ "provider.custom.headers.add": "Header hinzufügen",
+ "provider.custom.error.providerID.required": "Anbieter-ID ist erforderlich",
+ "provider.custom.error.providerID.format": "Verwenden Sie Kleinbuchstaben, Zahlen, Bindestriche oder Unterstriche",
+ "provider.custom.error.providerID.exists": "Diese Anbieter-ID existiert bereits",
+ "provider.custom.error.name.required": "Anzeigename ist erforderlich",
+ "provider.custom.error.baseURL.required": "Basis-URL ist erforderlich",
+ "provider.custom.error.baseURL.format": "Muss mit http:// oder https:// beginnen",
+ "provider.custom.error.required": "Erforderlich",
+ "provider.custom.error.duplicate": "Duplikat",
"settings.models.title": "Modelle",
"settings.models.description": "Modelleinstellungen können hier konfiguriert werden.",
"settings.agents.title": "Agenten",
@@ -829,6 +875,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "URL in die Zwischenablage kopiert",
"deviceAuth.toast.codeCopied": "Code in die Zwischenablage kopiert",
+ "deviceAuth.toast.errorCopied": "Fehler in die Zwischenablage kopiert",
"deviceAuth.status.initiating": "Anmeldung wird gestartet...",
"deviceAuth.title": "Bei Kilo Code anmelden",
"deviceAuth.step1": "Schritt 1: Diese URL öffnen",
@@ -840,8 +887,11 @@ export const dict = {
"deviceAuth.status.waiting": "Warten auf Autorisierung...",
"deviceAuth.status.success": "Anmeldung erfolgreich!",
"deviceAuth.status.failed": "Anmeldung fehlgeschlagen",
+ "deviceAuth.error.detailsTitle": "Anmeldefehler-Details",
"deviceAuth.status.cancelled": "Anmeldung abgebrochen",
"deviceAuth.action.tryAgain": "Erneut versuchen",
+ "deviceAuth.action.copyError": "Fehler kopieren",
+ "deviceAuth.action.showDetails": "Details anzeigen",
"common.retry": "Erneut versuchen",
"common.refresh": "Aktualisieren",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts
index dd11d65310..4dcb0a48ae 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts
@@ -561,6 +561,8 @@ export const dict = {
"The terminal connection was interrupted. This can happen when the server restarts.",
"common.closeTab": "Close tab",
+ "common.signIn": "Sign in",
+ "common.signOut": "Sign out",
"common.dismiss": "Dismiss",
"common.requestFailed": "Request failed",
"common.moreOptions": "More options",
@@ -716,10 +718,51 @@ export const dict = {
"settings.providers.section.connected": "Connected providers",
"settings.providers.connected.empty": "No connected providers",
"settings.providers.section.popular": "Popular providers",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Environment",
"settings.providers.tag.config": "Config",
"settings.providers.tag.custom": "Custom",
+ "settings.providers.tag.customProvider": "Custom provider",
"settings.providers.tag.other": "Other",
+ "settings.providers.connected.environmentDescription": "Connected from your environment variables",
+ "settings.providers.custom.description": "Add an OpenAI-compatible provider by base URL.",
+
+ "provider.custom.title": "Custom provider",
+ "provider.custom.description.prefix": "Configure an OpenAI-compatible provider. See the ",
+ "provider.custom.description.link": "provider config docs",
+ "provider.custom.description.suffix": ".",
+ "provider.custom.field.providerID.label": "Provider ID",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "Lowercase letters, numbers, hyphens, or underscores",
+ "provider.custom.field.name.label": "Display name",
+ "provider.custom.field.name.placeholder": "My AI Provider",
+ "provider.custom.field.baseURL.label": "Base URL",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "API key",
+ "provider.custom.field.apiKey.placeholder": "API key",
+ "provider.custom.field.apiKey.description": "Optional. Leave empty if you manage auth via headers.",
+ "provider.custom.models.label": "Models",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "Name",
+ "provider.custom.models.name.placeholder": "Display Name",
+ "provider.custom.models.remove": "Remove model",
+ "provider.custom.models.add": "Add model",
+ "provider.custom.headers.label": "Headers (optional)",
+ "provider.custom.headers.key.label": "Header",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "Value",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "Remove header",
+ "provider.custom.headers.add": "Add header",
+ "provider.custom.error.providerID.required": "Provider ID is required",
+ "provider.custom.error.providerID.format": "Use lowercase letters, numbers, hyphens, or underscores",
+ "provider.custom.error.providerID.exists": "That provider ID already exists",
+ "provider.custom.error.name.required": "Display name is required",
+ "provider.custom.error.baseURL.required": "Base URL is required",
+ "provider.custom.error.baseURL.format": "Must start with http:// or https://",
+ "provider.custom.error.required": "Required",
+ "provider.custom.error.duplicate": "Duplicate",
"settings.models.title": "Models",
"settings.models.description": "Model settings will be configurable here.",
"settings.agents.title": "Agents",
@@ -821,6 +864,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "URL copied to clipboard",
"deviceAuth.toast.codeCopied": "Code copied to clipboard",
+ "deviceAuth.toast.errorCopied": "Error copied to clipboard",
"deviceAuth.status.initiating": "Starting login...",
"deviceAuth.title": "Sign in to Kilo Code",
"deviceAuth.step1": "Step 1: Open this URL",
@@ -833,7 +877,10 @@ export const dict = {
"deviceAuth.status.success": "Login successful!",
"deviceAuth.status.failed": "Login failed",
"deviceAuth.status.cancelled": "Login cancelled",
+ "deviceAuth.action.copyError": "Copy error",
+ "deviceAuth.action.showDetails": "View details",
"deviceAuth.action.tryAgain": "Try Again",
+ "deviceAuth.error.detailsTitle": "Login error details",
"common.retry": "Retry",
"common.refresh": "Refresh",
@@ -1083,6 +1130,9 @@ export const dict = {
"settings.providers.enabled": "Enabled Providers (Allowlist)",
"settings.providers.enabled.description": "If set, only these providers will be available (exclusive allowlist)",
"settings.providers.notSet": "Not set (use server default)",
+ "settings.providers.custom.note": "Add an OpenAI-compatible provider by base URL.",
+ "settings.providers.search.placeholder": "Search providers",
+ "settings.providers.select.placeholder": "Select provider...",
"dialog.model.notSet": "Not set",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts
index f4feb9fc38..5474d53c6d 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts
@@ -559,6 +559,8 @@ export const dict = {
"terminal.connectionLost.description":
"La conexión del terminal se interrumpió. Esto puede ocurrir cuando el servidor se reinicia.",
"common.closeTab": "Cerrar pestaña",
+ "common.signIn": "Iniciar sesión",
+ "common.signOut": "Cerrar sesión",
"common.dismiss": "Descartar",
"common.requestFailed": "Solicitud fallida",
"common.moreOptions": "Más opciones",
@@ -716,13 +718,56 @@ export const dict = {
"settings.providers.section.connected": "Proveedores conectados",
"settings.providers.connected.empty": "No hay proveedores conectados",
"settings.providers.section.popular": "Proveedores populares",
+ "settings.providers.search.placeholder": "Buscar proveedores",
+ "settings.providers.select.placeholder": "Seleccionar proveedor...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Entorno",
"settings.providers.tag.config": "Configuración",
"settings.providers.tag.custom": "Personalizado",
"settings.providers.tag.other": "Otro",
+ "settings.providers.tag.customProvider": "Proveedor personalizado",
+ "settings.providers.connected.environmentDescription": "Conectado desde tus variables de entorno",
+ "settings.providers.custom.description": "Añade un proveedor compatible con OpenAI por URL base.",
"settings.providers.modeModels": "Modelo por modo",
+ "settings.providers.custom.note": "Agrega un proveedor compatible con OpenAI mediante URL base.",
"settings.providers.modeModels.description":
"Anula el modelo predeterminado para modos específicos. Si no se establece, se usa el modelo predeterminado global.",
+ "provider.custom.title": "Proveedor personalizado",
+ "provider.custom.description.prefix": "Configura un proveedor compatible con OpenAI. Consulta la ",
+ "provider.custom.description.link": "documentación de configuración de proveedores",
+ "provider.custom.description.suffix": ".",
+ "provider.custom.field.providerID.label": "ID del proveedor",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "Letras minúsculas, números, guiones o guiones bajos",
+ "provider.custom.field.name.label": "Nombre para mostrar",
+ "provider.custom.field.name.placeholder": "Mi proveedor de IA",
+ "provider.custom.field.baseURL.label": "URL base",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "Clave API",
+ "provider.custom.field.apiKey.placeholder": "Clave API",
+ "provider.custom.field.apiKey.description": "Opcional. Déjalo vacío si gestionas la autenticación mediante headers.",
+ "provider.custom.models.label": "Modelos",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "Nombre",
+ "provider.custom.models.name.placeholder": "Nombre para mostrar",
+ "provider.custom.models.remove": "Eliminar modelo",
+ "provider.custom.models.add": "Añadir modelo",
+ "provider.custom.headers.label": "Headers (opcional)",
+ "provider.custom.headers.key.label": "Header",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "Valor",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "Eliminar header",
+ "provider.custom.headers.add": "Añadir header",
+ "provider.custom.error.providerID.required": "El ID del proveedor es obligatorio",
+ "provider.custom.error.providerID.format": "Usa letras minúsculas, números, guiones o guiones bajos",
+ "provider.custom.error.providerID.exists": "Ese ID de proveedor ya existe",
+ "provider.custom.error.name.required": "El nombre para mostrar es obligatorio",
+ "provider.custom.error.baseURL.required": "La URL base es obligatoria",
+ "provider.custom.error.baseURL.format": "Debe empezar con http:// o https://",
+ "provider.custom.error.required": "Obligatorio",
+ "provider.custom.error.duplicate": "Duplicado",
"settings.models.title": "Modelos",
"settings.models.description": "La configuración de modelos estará disponible aquí.",
"settings.agents.title": "Agentes",
@@ -826,6 +871,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "URL copiada al portapapeles",
"deviceAuth.toast.codeCopied": "Código copiado al portapapeles",
+ "deviceAuth.toast.errorCopied": "Error copiado al portapapeles",
"deviceAuth.status.initiating": "Iniciando sesión...",
"deviceAuth.title": "Iniciar sesión en Kilo Code",
"deviceAuth.step1": "Paso 1: Abre esta URL",
@@ -837,8 +883,11 @@ export const dict = {
"deviceAuth.status.waiting": "Esperando autorización...",
"deviceAuth.status.success": "¡Inicio de sesión exitoso!",
"deviceAuth.status.failed": "Error en el inicio de sesión",
+ "deviceAuth.error.detailsTitle": "Detalles del error de inicio de sesión",
"deviceAuth.status.cancelled": "Inicio de sesión cancelado",
"deviceAuth.action.tryAgain": "Intentar de nuevo",
+ "deviceAuth.action.copyError": "Copiar error",
+ "deviceAuth.action.showDetails": "Ver detalles",
"common.retry": "Reintentar",
"common.refresh": "Actualizar",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts
index 1215208150..b1ceac19ef 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts
@@ -564,6 +564,8 @@ export const dict = {
"terminal.connectionLost.description":
"La connexion au terminal a été interrompue. Cela peut arriver lorsque le serveur redémarre.",
"common.closeTab": "Fermer l'onglet",
+ "common.signIn": "Se connecter",
+ "common.signOut": "Se déconnecter",
"common.dismiss": "Ignorer",
"common.requestFailed": "La demande a échoué",
"common.moreOptions": "Plus d'options",
@@ -720,13 +722,58 @@ export const dict = {
"settings.providers.section.connected": "Fournisseurs connectés",
"settings.providers.connected.empty": "Aucun fournisseur connecté",
"settings.providers.section.popular": "Fournisseurs populaires",
+ "settings.providers.search.placeholder": "Rechercher des fournisseurs",
+ "settings.providers.select.placeholder": "Sélectionner un fournisseur...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Environnement",
"settings.providers.tag.config": "Configuration",
"settings.providers.tag.custom": "Personnalisé",
"settings.providers.tag.other": "Autre",
+ "settings.providers.tag.customProvider": "Fournisseur personnalisé",
+ "settings.providers.connected.environmentDescription": "Connecté depuis vos variables d'environnement",
+ "settings.providers.custom.description": "Ajoutez un fournisseur compatible OpenAI par URL de base.",
"settings.providers.modeModels": "Modèle par mode",
+ "settings.providers.custom.note": "Ajoutez un fournisseur compatible OpenAI par URL de base.",
"settings.providers.modeModels.description":
"Remplacez le modèle par défaut pour des modes spécifiques. Si non défini, le modèle par défaut global est utilisé.",
+ "provider.custom.title": "Fournisseur personnalisé",
+ "provider.custom.description.prefix": "Configurez un fournisseur compatible OpenAI. Voir la ",
+ "provider.custom.description.link": "documentation de configuration des fournisseurs",
+ "provider.custom.description.suffix": ".",
+ "provider.custom.field.providerID.label": "ID du fournisseur",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "Lettres minuscules, chiffres, tirets ou underscores",
+ "provider.custom.field.name.label": "Nom d'affichage",
+ "provider.custom.field.name.placeholder": "Mon fournisseur IA",
+ "provider.custom.field.baseURL.label": "URL de base",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "Clé API",
+ "provider.custom.field.apiKey.placeholder": "Clé API",
+ "provider.custom.field.apiKey.description":
+ "Optionnel. Laissez vide si vous gérez l'authentification via les en-têtes.",
+ "provider.custom.models.label": "Modèles",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "Nom",
+ "provider.custom.models.name.placeholder": "Nom d'affichage",
+ "provider.custom.models.remove": "Supprimer le modèle",
+ "provider.custom.models.add": "Ajouter un modèle",
+ "provider.custom.headers.label": "En-têtes (optionnel)",
+ "provider.custom.headers.key.label": "En-tête",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "Valeur",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "Supprimer l'en-tête",
+ "provider.custom.headers.add": "Ajouter un en-tête",
+ "provider.custom.error.providerID.required": "L'ID du fournisseur est requis",
+ "provider.custom.error.providerID.format":
+ "Utilisez des lettres minuscules, des chiffres, des tirets ou des underscores",
+ "provider.custom.error.providerID.exists": "Cet ID de fournisseur existe déjà",
+ "provider.custom.error.name.required": "Le nom d'affichage est requis",
+ "provider.custom.error.baseURL.required": "L'URL de base est requise",
+ "provider.custom.error.baseURL.format": "Doit commencer par http:// ou https://",
+ "provider.custom.error.required": "Requis",
+ "provider.custom.error.duplicate": "Doublon",
"settings.models.title": "Modèles",
"settings.models.description": "Les paramètres des modèles seront configurables ici.",
"settings.agents.title": "Agents",
@@ -831,6 +878,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "URL copiée dans le presse-papier",
"deviceAuth.toast.codeCopied": "Code copié dans le presse-papier",
+ "deviceAuth.toast.errorCopied": "Erreur copiée dans le presse-papiers",
"deviceAuth.status.initiating": "Démarrage de la connexion...",
"deviceAuth.title": "Se connecter à Kilo Code",
"deviceAuth.step1": "Étape 1 : Ouvrez cette URL",
@@ -842,8 +890,11 @@ export const dict = {
"deviceAuth.status.waiting": "En attente d'autorisation...",
"deviceAuth.status.success": "Connexion réussie !",
"deviceAuth.status.failed": "Échec de la connexion",
+ "deviceAuth.error.detailsTitle": "Détails de l'erreur de connexion",
"deviceAuth.status.cancelled": "Connexion annulée",
"deviceAuth.action.tryAgain": "Réessayer",
+ "deviceAuth.action.copyError": "Copier l'erreur",
+ "deviceAuth.action.showDetails": "Voir les détails",
"common.retry": "Réessayer",
"common.refresh": "Actualiser",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts
index 20f7f65794..ed98d5d44a 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts
@@ -555,6 +555,8 @@ export const dict = {
"terminal.connectionLost.description":
"ターミナルの接続が中断されました。これはサーバーが再起動したときに発生することがあります。",
"common.closeTab": "タブを閉じる",
+ "common.signIn": "サインイン",
+ "common.signOut": "サインアウト",
"common.dismiss": "閉じる",
"common.requestFailed": "リクエスト失敗",
"common.moreOptions": "その他のオプション",
@@ -709,13 +711,56 @@ export const dict = {
"settings.providers.section.connected": "接続済みプロバイダー",
"settings.providers.connected.empty": "接続済みプロバイダーはありません",
"settings.providers.section.popular": "人気のプロバイダー",
+ "settings.providers.search.placeholder": "プロバイダーを検索",
+ "settings.providers.select.placeholder": "プロバイダーを選択...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "環境",
"settings.providers.tag.config": "設定",
"settings.providers.tag.custom": "カスタム",
"settings.providers.tag.other": "その他",
+ "settings.providers.tag.customProvider": "カスタムプロバイダー",
+ "settings.providers.connected.environmentDescription": "環境変数から接続されています",
+ "settings.providers.custom.description": "ベースURLでOpenAI互換プロバイダーを追加します。",
"settings.providers.modeModels": "モードごとのモデル",
+ "settings.providers.custom.note": "Base URL で OpenAI 互換プロバイダーを追加します。",
"settings.providers.modeModels.description":
"特定のモードのデフォルトモデルを上書きします。設定されていない場合、グローバルデフォルトモデルが使用されます。",
+ "provider.custom.title": "カスタムプロバイダー",
+ "provider.custom.description.prefix": "OpenAI互換プロバイダーを設定します。",
+ "provider.custom.description.link": "プロバイダー設定ドキュメント",
+ "provider.custom.description.suffix": "を参照してください。",
+ "provider.custom.field.providerID.label": "プロバイダーID",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "小文字、数字、ハイフン、アンダースコア",
+ "provider.custom.field.name.label": "表示名",
+ "provider.custom.field.name.placeholder": "マイAIプロバイダー",
+ "provider.custom.field.baseURL.label": "ベースURL",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "APIキー",
+ "provider.custom.field.apiKey.placeholder": "APIキー",
+ "provider.custom.field.apiKey.description": "任意。ヘッダーで認証を管理する場合は空のままにしてください。",
+ "provider.custom.models.label": "モデル",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "名前",
+ "provider.custom.models.name.placeholder": "表示名",
+ "provider.custom.models.remove": "モデルを削除",
+ "provider.custom.models.add": "モデルを追加",
+ "provider.custom.headers.label": "ヘッダー(任意)",
+ "provider.custom.headers.key.label": "ヘッダー",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "値",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "ヘッダーを削除",
+ "provider.custom.headers.add": "ヘッダーを追加",
+ "provider.custom.error.providerID.required": "プロバイダーIDは必須です",
+ "provider.custom.error.providerID.format": "小文字、数字、ハイフン、アンダースコアを使用してください",
+ "provider.custom.error.providerID.exists": "そのプロバイダーIDは既に存在します",
+ "provider.custom.error.name.required": "表示名は必須です",
+ "provider.custom.error.baseURL.required": "ベースURLは必須です",
+ "provider.custom.error.baseURL.format": "http:// または https:// で始まる必要があります",
+ "provider.custom.error.required": "必須",
+ "provider.custom.error.duplicate": "重複",
"settings.models.title": "モデル",
"settings.models.description": "モデル設定はここで構成できます。",
"settings.agents.title": "エージェント",
@@ -818,6 +863,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "URLをクリップボードにコピーしました",
"deviceAuth.toast.codeCopied": "コードをクリップボードにコピーしました",
+ "deviceAuth.toast.errorCopied": "エラーがクリップボードにコピーされました",
"deviceAuth.status.initiating": "ログインを開始しています...",
"deviceAuth.title": "Kilo Codeにサインイン",
"deviceAuth.step1": "ステップ1:このURLを開く",
@@ -829,8 +875,11 @@ export const dict = {
"deviceAuth.status.waiting": "認証を待っています...",
"deviceAuth.status.success": "ログイン成功!",
"deviceAuth.status.failed": "ログイン失敗",
+ "deviceAuth.error.detailsTitle": "ログインエラーの詳細",
"deviceAuth.status.cancelled": "ログインがキャンセルされました",
"deviceAuth.action.tryAgain": "再試行",
+ "deviceAuth.action.copyError": "エラーをコピー",
+ "deviceAuth.action.showDetails": "詳細を表示",
"common.retry": "再試行",
"common.refresh": "更新",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts
index d8ae5a8c57..f8d50dc43c 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts
@@ -557,6 +557,8 @@ export const dict = {
"terminal.connectionLost.description":
"터미널 연결이 중단되었습니다. 서버가 재시작하면 이런 일이 발생할 수 있습니다.",
"common.closeTab": "탭 닫기",
+ "common.signIn": "로그인",
+ "common.signOut": "로그아웃",
"common.dismiss": "닫기",
"common.requestFailed": "요청 실패",
"common.moreOptions": "더 많은 옵션",
@@ -710,13 +712,56 @@ export const dict = {
"settings.providers.section.connected": "연결된 공급자",
"settings.providers.connected.empty": "연결된 공급자 없음",
"settings.providers.section.popular": "인기 공급자",
+ "settings.providers.search.placeholder": "공급자 검색",
+ "settings.providers.select.placeholder": "공급자 선택...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "환경",
"settings.providers.tag.config": "구성",
"settings.providers.tag.custom": "사용자 지정",
"settings.providers.tag.other": "기타",
+ "settings.providers.tag.customProvider": "사용자 정의 공급자",
+ "settings.providers.connected.environmentDescription": "환경 변수에서 연결됨",
+ "settings.providers.custom.description": "기본 URL로 OpenAI 호환 공급자를 추가합니다.",
"settings.providers.modeModels": "모드별 모델",
+ "settings.providers.custom.note": "Base URL로 OpenAI 호환 공급자를 추가합니다.",
"settings.providers.modeModels.description":
"특정 모드의 기본 모델을 재정의합니다. 설정하지 않으면 전역 기본 모델이 사용됩니다.",
+ "provider.custom.title": "사용자 정의 공급자",
+ "provider.custom.description.prefix": "OpenAI 호환 공급자를 구성합니다. ",
+ "provider.custom.description.link": "공급자 구성 문서",
+ "provider.custom.description.suffix": "를 참조하세요.",
+ "provider.custom.field.providerID.label": "공급자 ID",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "소문자, 숫자, 하이픈 또는 밑줄",
+ "provider.custom.field.name.label": "표시 이름",
+ "provider.custom.field.name.placeholder": "내 AI 공급자",
+ "provider.custom.field.baseURL.label": "기본 URL",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "API 키",
+ "provider.custom.field.apiKey.placeholder": "API 키",
+ "provider.custom.field.apiKey.description": "선택사항. 헤더로 인증을 관리하는 경우 비워두세요.",
+ "provider.custom.models.label": "모델",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "이름",
+ "provider.custom.models.name.placeholder": "표시 이름",
+ "provider.custom.models.remove": "모델 제거",
+ "provider.custom.models.add": "모델 추가",
+ "provider.custom.headers.label": "헤더 (선택사항)",
+ "provider.custom.headers.key.label": "헤더",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "값",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "헤더 제거",
+ "provider.custom.headers.add": "헤더 추가",
+ "provider.custom.error.providerID.required": "공급자 ID는 필수입니다",
+ "provider.custom.error.providerID.format": "소문자, 숫자, 하이픈 또는 밑줄을 사용하세요",
+ "provider.custom.error.providerID.exists": "해당 공급자 ID가 이미 존재합니다",
+ "provider.custom.error.name.required": "표시 이름은 필수입니다",
+ "provider.custom.error.baseURL.required": "기본 URL은 필수입니다",
+ "provider.custom.error.baseURL.format": "http:// 또는 https://로 시작해야 합니다",
+ "provider.custom.error.required": "필수",
+ "provider.custom.error.duplicate": "중복",
"settings.models.title": "모델",
"settings.models.description": "모델 설정은 여기서 구성할 수 있습니다.",
"settings.agents.title": "에이전트",
@@ -818,6 +863,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "URL이 클립보드에 복사되었습니다",
"deviceAuth.toast.codeCopied": "코드가 클립보드에 복사되었습니다",
+ "deviceAuth.toast.errorCopied": "오류가 클립보드에 복사되었습니다",
"deviceAuth.status.initiating": "로그인 시작 중...",
"deviceAuth.title": "Kilo Code에 로그인",
"deviceAuth.step1": "1단계: 이 URL을 여세요",
@@ -829,8 +875,11 @@ export const dict = {
"deviceAuth.status.waiting": "인증 대기 중...",
"deviceAuth.status.success": "로그인 성공!",
"deviceAuth.status.failed": "로그인 실패",
+ "deviceAuth.error.detailsTitle": "로그인 오류 상세 정보",
"deviceAuth.status.cancelled": "로그인 취소됨",
"deviceAuth.action.tryAgain": "다시 시도",
+ "deviceAuth.action.copyError": "오류 복사",
+ "deviceAuth.action.showDetails": "자세히 보기",
"common.retry": "재시도",
"common.refresh": "새로고침",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts
index 628bfb8343..297770ca4f 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts
@@ -560,6 +560,8 @@ export const dict = {
"Terminalforbindelsen ble avbrutt. Dette kan skje når serveren starter på nytt.",
"common.closeTab": "Lukk fane",
+ "common.signIn": "Logg inn",
+ "common.signOut": "Logg ut",
"common.dismiss": "Avvis",
"common.requestFailed": "Forespørsel mislyktes",
"common.moreOptions": "Flere alternativer",
@@ -714,13 +716,56 @@ export const dict = {
"settings.providers.section.connected": "Tilkoblede leverandører",
"settings.providers.connected.empty": "Ingen tilkoblede leverandører",
"settings.providers.section.popular": "Populære leverandører",
+ "settings.providers.search.placeholder": "Søk etter leverandører",
+ "settings.providers.select.placeholder": "Velg leverandør...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Miljø",
"settings.providers.tag.config": "Konfigurasjon",
"settings.providers.tag.custom": "Tilpasset",
"settings.providers.tag.other": "Annet",
+ "settings.providers.tag.customProvider": "Egendefinert leverandør",
+ "settings.providers.connected.environmentDescription": "Koblet til fra dine miljøvariabler",
+ "settings.providers.custom.description": "Legg til en OpenAI-kompatibel leverandør via basis-URL.",
"settings.providers.modeModels": "Modell per modus",
+ "settings.providers.custom.note": "Legg til en OpenAI-kompatibel leverandør via basis-URL.",
"settings.providers.modeModels.description":
"Overstyr standardmodellen for bestemte moduser. Hvis ikke angitt, brukes den globale standardmodellen.",
+ "provider.custom.title": "Egendefinert leverandør",
+ "provider.custom.description.prefix": "Konfigurer en OpenAI-kompatibel leverandør. Se ",
+ "provider.custom.description.link": "dokumentasjon for leverandørkonfigurasjon",
+ "provider.custom.description.suffix": ".",
+ "provider.custom.field.providerID.label": "Leverandør-ID",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "Små bokstaver, tall, bindestreker eller understreker",
+ "provider.custom.field.name.label": "Visningsnavn",
+ "provider.custom.field.name.placeholder": "Min AI-leverandør",
+ "provider.custom.field.baseURL.label": "Basis-URL",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "API-nøkkel",
+ "provider.custom.field.apiKey.placeholder": "API-nøkkel",
+ "provider.custom.field.apiKey.description": "Valgfritt. La stå tomt hvis du administrerer autentisering via headere.",
+ "provider.custom.models.label": "Modeller",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "Navn",
+ "provider.custom.models.name.placeholder": "Visningsnavn",
+ "provider.custom.models.remove": "Fjern modell",
+ "provider.custom.models.add": "Legg til modell",
+ "provider.custom.headers.label": "Headere (valgfritt)",
+ "provider.custom.headers.key.label": "Header",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "Verdi",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "Fjern header",
+ "provider.custom.headers.add": "Legg til header",
+ "provider.custom.error.providerID.required": "Leverandør-ID er påkrevd",
+ "provider.custom.error.providerID.format": "Bruk små bokstaver, tall, bindestreker eller understreker",
+ "provider.custom.error.providerID.exists": "Den leverandør-IDen eksisterer allerede",
+ "provider.custom.error.name.required": "Visningsnavn er påkrevd",
+ "provider.custom.error.baseURL.required": "Basis-URL er påkrevd",
+ "provider.custom.error.baseURL.format": "Må starte med http:// eller https://",
+ "provider.custom.error.required": "Påkrevd",
+ "provider.custom.error.duplicate": "Duplikat",
"settings.models.title": "Modeller",
"settings.models.description": "Modellinnstillinger vil kunne konfigureres her.",
"settings.agents.title": "Agenter",
@@ -823,6 +868,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "URL kopiert til utklippstavlen",
"deviceAuth.toast.codeCopied": "Kode kopiert til utklippstavlen",
+ "deviceAuth.toast.errorCopied": "Feil kopiert til utklippstavlen",
"deviceAuth.status.initiating": "Starter pålogging...",
"deviceAuth.title": "Logg inn på Kilo Code",
"deviceAuth.step1": "Trinn 1: Åpne denne URLen",
@@ -834,8 +880,11 @@ export const dict = {
"deviceAuth.status.waiting": "Venter på autorisasjon...",
"deviceAuth.status.success": "Pålogging vellykket!",
"deviceAuth.status.failed": "Pålogging mislyktes",
+ "deviceAuth.error.detailsTitle": "Detaljer om påloggingsfeil",
"deviceAuth.status.cancelled": "Pålogging avbrutt",
"deviceAuth.action.tryAgain": "Prøv igjen",
+ "deviceAuth.action.copyError": "Kopier feil",
+ "deviceAuth.action.showDetails": "Vis detaljer",
"common.retry": "Prøv igjen",
"common.refresh": "Oppdater",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts
index 4917a4f87c..cebf738429 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts
@@ -558,6 +558,8 @@ export const dict = {
"Połączenie z terminalem zostało przerwane. Może się to zdarzyć przy restarcie serwera.",
"common.closeTab": "Zamknij kartę",
+ "common.signIn": "Zaloguj się",
+ "common.signOut": "Wyloguj się",
"common.dismiss": "Odrzuć",
"common.requestFailed": "Żądanie nie powiodło się",
"common.moreOptions": "Więcej opcji",
@@ -713,13 +715,57 @@ export const dict = {
"settings.providers.section.connected": "Połączeni dostawcy",
"settings.providers.connected.empty": "Brak połączonych dostawców",
"settings.providers.section.popular": "Popularni dostawcy",
+ "settings.providers.search.placeholder": "Szukaj dostawców",
+ "settings.providers.select.placeholder": "Wybierz dostawcę...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Środowisko",
"settings.providers.tag.config": "Konfiguracja",
"settings.providers.tag.custom": "Niestandardowe",
"settings.providers.tag.other": "Inne",
+ "settings.providers.tag.customProvider": "Niestandardowy dostawca",
+ "settings.providers.connected.environmentDescription": "Połączony z twoich zmiennych środowiskowych",
+ "settings.providers.custom.description": "Dodaj dostawcę kompatybilnego z OpenAI przez bazowy URL.",
"settings.providers.modeModels": "Model na tryb",
+ "settings.providers.custom.note": "Dodaj dostawcę kompatybilnego z OpenAI przez bazowy URL.",
"settings.providers.modeModels.description":
"Zastąp domyślny model dla określonych trybów. Jeśli nie ustawiono, używany jest globalny domyślny model.",
+ "provider.custom.title": "Niestandardowy dostawca",
+ "provider.custom.description.prefix": "Skonfiguruj dostawcę kompatybilnego z OpenAI. Zobacz ",
+ "provider.custom.description.link": "dokumentację konfiguracji dostawcy",
+ "provider.custom.description.suffix": ".",
+ "provider.custom.field.providerID.label": "ID dostawcy",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "Małe litery, cyfry, myślniki lub podkreślenia",
+ "provider.custom.field.name.label": "Nazwa wyświetlana",
+ "provider.custom.field.name.placeholder": "Mój dostawca AI",
+ "provider.custom.field.baseURL.label": "Bazowy URL",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "Klucz API",
+ "provider.custom.field.apiKey.placeholder": "Klucz API",
+ "provider.custom.field.apiKey.description":
+ "Opcjonalnie. Pozostaw puste, jeśli zarządzasz uwierzytelnianiem przez nagłówki.",
+ "provider.custom.models.label": "Modele",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "Nazwa",
+ "provider.custom.models.name.placeholder": "Nazwa wyświetlana",
+ "provider.custom.models.remove": "Usuń model",
+ "provider.custom.models.add": "Dodaj model",
+ "provider.custom.headers.label": "Nagłówki (opcjonalnie)",
+ "provider.custom.headers.key.label": "Nagłówek",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "Wartość",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "Usuń nagłówek",
+ "provider.custom.headers.add": "Dodaj nagłówek",
+ "provider.custom.error.providerID.required": "ID dostawcy jest wymagane",
+ "provider.custom.error.providerID.format": "Użyj małych liter, cyfr, myślników lub podkreśleń",
+ "provider.custom.error.providerID.exists": "Ten ID dostawcy już istnieje",
+ "provider.custom.error.name.required": "Nazwa wyświetlana jest wymagana",
+ "provider.custom.error.baseURL.required": "Bazowy URL jest wymagany",
+ "provider.custom.error.baseURL.format": "Musi zaczynać się od http:// lub https://",
+ "provider.custom.error.required": "Wymagane",
+ "provider.custom.error.duplicate": "Duplikat",
"settings.models.title": "Modele",
"settings.models.description": "Ustawienia modeli będą tutaj konfigurowalne.",
"settings.agents.title": "Agenci",
@@ -821,6 +867,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "URL skopiowany do schowka",
"deviceAuth.toast.codeCopied": "Kod skopiowany do schowka",
+ "deviceAuth.toast.errorCopied": "Błąd skopiowany do schowka",
"deviceAuth.status.initiating": "Rozpoczynanie logowania...",
"deviceAuth.title": "Zaloguj się do Kilo Code",
"deviceAuth.step1": "Krok 1: Otwórz ten URL",
@@ -832,8 +879,11 @@ export const dict = {
"deviceAuth.status.waiting": "Oczekiwanie na autoryzację...",
"deviceAuth.status.success": "Logowanie powiodło się!",
"deviceAuth.status.failed": "Logowanie nie powiodło się",
+ "deviceAuth.error.detailsTitle": "Szczegóły błędu logowania",
"deviceAuth.status.cancelled": "Logowanie anulowane",
"deviceAuth.action.tryAgain": "Spróbuj ponownie",
+ "deviceAuth.action.copyError": "Kopiuj błąd",
+ "deviceAuth.action.showDetails": "Pokaż szczegóły",
"common.retry": "Ponów",
"common.refresh": "Odśwież",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts
index 243394225b..859c415ea7 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts
@@ -560,6 +560,8 @@ export const dict = {
"Соединение с терминалом прервано. Это может произойти при перезапуске сервера.",
"common.closeTab": "Закрыть вкладку",
+ "common.signIn": "Войти",
+ "common.signOut": "Выйти",
"common.dismiss": "Закрыть",
"common.requestFailed": "Запрос не выполнен",
"common.moreOptions": "Дополнительные опции",
@@ -716,13 +718,57 @@ export const dict = {
"settings.providers.section.connected": "Подключённые провайдеры",
"settings.providers.connected.empty": "Нет подключённых провайдеров",
"settings.providers.section.popular": "Популярные провайдеры",
+ "settings.providers.search.placeholder": "Поиск провайдеров",
+ "settings.providers.select.placeholder": "Выберите провайдера...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "Среда",
"settings.providers.tag.config": "Конфигурация",
"settings.providers.tag.custom": "Пользовательский",
"settings.providers.tag.other": "Другое",
+ "settings.providers.tag.customProvider": "Пользовательский провайдер",
+ "settings.providers.connected.environmentDescription": "Подключён из ваших переменных окружения",
+ "settings.providers.custom.description": "Добавьте OpenAI-совместимый провайдер по базовому URL.",
"settings.providers.modeModels": "Модель для режима",
+ "settings.providers.custom.note": "Добавьте OpenAI-совместимого провайдера по базовому URL.",
"settings.providers.modeModels.description":
"Переопределите модель по умолчанию для определённых режимов. Если не задано, используется глобальная модель по умолчанию.",
+ "provider.custom.title": "Пользовательский провайдер",
+ "provider.custom.description.prefix": "Настройте провайдер, совместимый с OpenAI. См. ",
+ "provider.custom.description.link": "документацию по настройке провайдера",
+ "provider.custom.description.suffix": ".",
+ "provider.custom.field.providerID.label": "ID провайдера",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "Строчные буквы, цифры, дефисы или подчёркивания",
+ "provider.custom.field.name.label": "Отображаемое имя",
+ "provider.custom.field.name.placeholder": "Мой AI-провайдер",
+ "provider.custom.field.baseURL.label": "Базовый URL",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "API-ключ",
+ "provider.custom.field.apiKey.placeholder": "API-ключ",
+ "provider.custom.field.apiKey.description":
+ "Необязательно. Оставьте пустым, если вы управляете аутентификацией через заголовки.",
+ "provider.custom.models.label": "Модели",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "Название",
+ "provider.custom.models.name.placeholder": "Отображаемое имя",
+ "provider.custom.models.remove": "Удалить модель",
+ "provider.custom.models.add": "Добавить модель",
+ "provider.custom.headers.label": "Заголовки (необязательно)",
+ "provider.custom.headers.key.label": "Заголовок",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "Значение",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "Удалить заголовок",
+ "provider.custom.headers.add": "Добавить заголовок",
+ "provider.custom.error.providerID.required": "ID провайдера обязателен",
+ "provider.custom.error.providerID.format": "Используйте строчные буквы, цифры, дефисы или подчёркивания",
+ "provider.custom.error.providerID.exists": "Такой ID провайдера уже существует",
+ "provider.custom.error.name.required": "Отображаемое имя обязательно",
+ "provider.custom.error.baseURL.required": "Базовый URL обязателен",
+ "provider.custom.error.baseURL.format": "Должен начинаться с http:// или https://",
+ "provider.custom.error.required": "Обязательно",
+ "provider.custom.error.duplicate": "Дубликат",
"settings.models.title": "Модели",
"settings.models.description": "Настройки моделей будут доступны здесь.",
"settings.agents.title": "Агенты",
@@ -825,6 +871,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "URL скопирован в буфер обмена",
"deviceAuth.toast.codeCopied": "Код скопирован в буфер обмена",
+ "deviceAuth.toast.errorCopied": "Ошибка скопирована в буфер обмена",
"deviceAuth.status.initiating": "Начинаем вход...",
"deviceAuth.title": "Войти в Kilo Code",
"deviceAuth.step1": "Шаг 1: Откройте этот URL",
@@ -836,8 +883,11 @@ export const dict = {
"deviceAuth.status.waiting": "Ожидание авторизации...",
"deviceAuth.status.success": "Вход выполнен успешно!",
"deviceAuth.status.failed": "Ошибка входа",
+ "deviceAuth.error.detailsTitle": "Подробности ошибки входа",
"deviceAuth.status.cancelled": "Вход отменён",
"deviceAuth.action.tryAgain": "Попробовать снова",
+ "deviceAuth.action.copyError": "Копировать ошибку",
+ "deviceAuth.action.showDetails": "Посмотреть детали",
"common.retry": "Повторить",
"common.refresh": "Обновить",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts
index 84760f2ffc..b49e134107 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts
@@ -554,6 +554,8 @@ export const dict = {
"terminal.connectionLost.description": "การเชื่อมต่อเทอร์มินัลถูกขัดจังหวะ อาจเกิดขึ้นเมื่อเซิร์ฟเวอร์รีสตาร์ท",
"common.closeTab": "ปิดแท็บ",
+ "common.signIn": "เข้าสู่ระบบ",
+ "common.signOut": "ออกจากระบบ",
"common.dismiss": "ปิด",
"common.requestFailed": "คำขอล้มเหลว",
"common.moreOptions": "ตัวเลือกเพิ่มเติม",
@@ -707,13 +709,56 @@ export const dict = {
"settings.providers.section.connected": "ผู้ให้บริการที่เชื่อมต่อ",
"settings.providers.connected.empty": "ไม่มีผู้ให้บริการที่เชื่อมต่อ",
"settings.providers.section.popular": "ผู้ให้บริการยอดนิยม",
+ "settings.providers.search.placeholder": "ค้นหาผู้ให้บริการ",
+ "settings.providers.select.placeholder": "เลือกผู้ให้บริการ...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "สภาพแวดล้อม",
"settings.providers.tag.config": "กำหนดค่า",
"settings.providers.tag.custom": "กำหนดเอง",
"settings.providers.tag.other": "อื่น ๆ",
+ "settings.providers.tag.customProvider": "ผู้ให้บริการที่กำหนดเอง",
+ "settings.providers.connected.environmentDescription": "เชื่อมต่อจากตัวแปรสภาพแวดล้อมของคุณ",
+ "settings.providers.custom.description": "เพิ่มผู้ให้บริการที่เข้ากันได้กับ OpenAI ด้วย URL พื้นฐาน",
"settings.providers.modeModels": "โมเดลต่อโหมด",
+ "settings.providers.custom.note": "เพิ่มผู้ให้บริการที่รองรับ OpenAI ด้วย Base URL",
"settings.providers.modeModels.description":
"แทนที่โมเดลเริ่มต้นสำหรับโหมดที่กำหนด หากไม่ได้ตั้งค่า จะใช้โมเดลเริ่มต้นทั่วไป",
+ "provider.custom.title": "ผู้ให้บริการที่กำหนดเอง",
+ "provider.custom.description.prefix": "กำหนดค่าผู้ให้บริการที่เข้ากันได้กับ OpenAI ดู",
+ "provider.custom.description.link": "เอกสารการกำหนดค่าผู้ให้บริการ",
+ "provider.custom.description.suffix": "",
+ "provider.custom.field.providerID.label": "ID ผู้ให้บริการ",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "ตัวอักษรพิมพ์เล็ก ตัวเลข ขีดกลาง หรือขีดล่าง",
+ "provider.custom.field.name.label": "ชื่อที่แสดง",
+ "provider.custom.field.name.placeholder": "ผู้ให้บริการ AI ของฉัน",
+ "provider.custom.field.baseURL.label": "URL พื้นฐาน",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "คีย์ API",
+ "provider.custom.field.apiKey.placeholder": "คีย์ API",
+ "provider.custom.field.apiKey.description": "ไม่จำเป็น เว้นว่างไว้หากคุณจัดการการรับรองความถูกต้องผ่านส่วนหัว",
+ "provider.custom.models.label": "โมเดล",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "ชื่อ",
+ "provider.custom.models.name.placeholder": "ชื่อที่แสดง",
+ "provider.custom.models.remove": "ลบโมเดล",
+ "provider.custom.models.add": "เพิ่มโมเดล",
+ "provider.custom.headers.label": "ส่วนหัว (ไม่จำเป็น)",
+ "provider.custom.headers.key.label": "ส่วนหัว",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "ค่า",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "ลบส่วนหัว",
+ "provider.custom.headers.add": "เพิ่มส่วนหัว",
+ "provider.custom.error.providerID.required": "ต้องระบุ ID ผู้ให้บริการ",
+ "provider.custom.error.providerID.format": "ใช้ตัวอักษรพิมพ์เล็ก ตัวเลข ขีดกลาง หรือขีดล่าง",
+ "provider.custom.error.providerID.exists": "ID ผู้ให้บริการนี้มีอยู่แล้ว",
+ "provider.custom.error.name.required": "ต้องระบุชื่อที่แสดง",
+ "provider.custom.error.baseURL.required": "ต้องระบุ URL พื้นฐาน",
+ "provider.custom.error.baseURL.format": "ต้องขึ้นต้นด้วย http:// หรือ https://",
+ "provider.custom.error.required": "จำเป็น",
+ "provider.custom.error.duplicate": "ซ้ำ",
"settings.models.title": "โมเดล",
"settings.models.description": "การตั้งค่าโมเดลจะสามารถกำหนดค่าได้ที่นี่",
"settings.agents.title": "เอเจนต์",
@@ -815,6 +860,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "คัดลอก URL ไปยังคลิปบอร์ดแล้ว",
"deviceAuth.toast.codeCopied": "คัดลอกรหัสไปยังคลิปบอร์ดแล้ว",
+ "deviceAuth.toast.errorCopied": "คัดลอกข้อผิดพลาดไปยังคลิปบอร์ดแล้ว",
"deviceAuth.status.initiating": "กำลังเริ่มเข้าสู่ระบบ...",
"deviceAuth.title": "เข้าสู่ระบบ Kilo Code",
"deviceAuth.step1": "ขั้นตอนที่ 1: เปิด URL นี้",
@@ -826,8 +872,11 @@ export const dict = {
"deviceAuth.status.waiting": "กำลังรอการอนุญาต...",
"deviceAuth.status.success": "เข้าสู่ระบบสำเร็จ!",
"deviceAuth.status.failed": "เข้าสู่ระบบล้มเหลว",
+ "deviceAuth.error.detailsTitle": "รายละเอียดข้อผิดพลาดการเข้าสู่ระบบ",
"deviceAuth.status.cancelled": "ยกเลิกการเข้าสู่ระบบ",
"deviceAuth.action.tryAgain": "ลองอีกครั้ง",
+ "deviceAuth.action.copyError": "คัดลอกข้อผิดพลาด",
+ "deviceAuth.action.showDetails": "ดูรายละเอียด",
"common.retry": "ลองอีกครั้ง",
"common.refresh": "รีเฟรช",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts
index 140cf5ea00..c6d2c3a12b 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts
@@ -548,6 +548,8 @@ export const dict = {
"terminal.connectionLost.title": "连接已丢失",
"terminal.connectionLost.description": "终端连接已中断。这可能发生在服务器重启时。",
"common.closeTab": "关闭标签页",
+ "common.signIn": "登录",
+ "common.signOut": "退出登录",
"common.dismiss": "忽略",
"common.requestFailed": "请求失败",
"common.moreOptions": "更多选项",
@@ -700,12 +702,55 @@ export const dict = {
"settings.providers.section.connected": "已连接的提供商",
"settings.providers.connected.empty": "没有已连接的提供商",
"settings.providers.section.popular": "热门提供商",
+ "settings.providers.search.placeholder": "搜索提供商",
+ "settings.providers.select.placeholder": "选择提供商...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "环境",
"settings.providers.tag.config": "配置",
"settings.providers.tag.custom": "自定义",
"settings.providers.tag.other": "其他",
+ "settings.providers.tag.customProvider": "自定义提供商",
+ "settings.providers.connected.environmentDescription": "从您的环境变量连接",
+ "settings.providers.custom.description": "通过基础 URL 添加 OpenAI 兼容的提供商。",
"settings.providers.modeModels": "按模式选择模型",
+ "settings.providers.custom.note": "通过 Base URL 添加 OpenAI 兼容提供商。",
"settings.providers.modeModels.description": "为特定模式覆盖默认模型。如果未设置,将使用全局默认模型。",
+ "provider.custom.title": "自定义提供商",
+ "provider.custom.description.prefix": "配置 OpenAI 兼容的提供商。请参阅",
+ "provider.custom.description.link": "提供商配置文档",
+ "provider.custom.description.suffix": "。",
+ "provider.custom.field.providerID.label": "提供商 ID",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "小写字母、数字、连字符或下划线",
+ "provider.custom.field.name.label": "显示名称",
+ "provider.custom.field.name.placeholder": "我的 AI 提供商",
+ "provider.custom.field.baseURL.label": "基础 URL",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "API 密钥",
+ "provider.custom.field.apiKey.placeholder": "API 密钥",
+ "provider.custom.field.apiKey.description": "可选。如果您通过请求头管理认证,请留空。",
+ "provider.custom.models.label": "模型",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "名称",
+ "provider.custom.models.name.placeholder": "显示名称",
+ "provider.custom.models.remove": "移除模型",
+ "provider.custom.models.add": "添加模型",
+ "provider.custom.headers.label": "请求头(可选)",
+ "provider.custom.headers.key.label": "请求头",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "值",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "移除请求头",
+ "provider.custom.headers.add": "添加请求头",
+ "provider.custom.error.providerID.required": "提供商 ID 为必填项",
+ "provider.custom.error.providerID.format": "使用小写字母、数字、连字符或下划线",
+ "provider.custom.error.providerID.exists": "该提供商 ID 已存在",
+ "provider.custom.error.name.required": "显示名称为必填项",
+ "provider.custom.error.baseURL.required": "基础 URL 为必填项",
+ "provider.custom.error.baseURL.format": "必须以 http:// 或 https:// 开头",
+ "provider.custom.error.required": "必填",
+ "provider.custom.error.duplicate": "重复",
"settings.models.title": "模型",
"settings.models.description": "模型设置将在此处可配置。",
"settings.agents.title": "智能体",
@@ -807,6 +852,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "URL 已复制到剪贴板",
"deviceAuth.toast.codeCopied": "代码已复制到剪贴板",
+ "deviceAuth.toast.errorCopied": "错误已复制到剪贴板",
"deviceAuth.status.initiating": "正在启动登录...",
"deviceAuth.title": "登录 Kilo Code",
"deviceAuth.step1": "步骤 1:打开此 URL",
@@ -818,8 +864,11 @@ export const dict = {
"deviceAuth.status.waiting": "等待授权中...",
"deviceAuth.status.success": "登录成功!",
"deviceAuth.status.failed": "登录失败",
+ "deviceAuth.error.detailsTitle": "登录错误详情",
"deviceAuth.status.cancelled": "登录已取消",
"deviceAuth.action.tryAgain": "重试",
+ "deviceAuth.action.copyError": "复制错误",
+ "deviceAuth.action.showDetails": "查看详情",
"common.retry": "重试",
"common.refresh": "刷新",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts
index 206445b845..40213cc32a 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts
@@ -549,6 +549,8 @@ export const dict = {
"terminal.connectionLost.title": "連線中斷",
"terminal.connectionLost.description": "終端機連線已中斷。這可能會在伺服器重新啟動時發生。",
"common.closeTab": "關閉標籤頁",
+ "common.signIn": "登入",
+ "common.signOut": "登出",
"common.dismiss": "忽略",
"common.requestFailed": "要求失敗",
"common.moreOptions": "更多選項",
@@ -702,12 +704,55 @@ export const dict = {
"settings.providers.section.connected": "已連線的供應商",
"settings.providers.connected.empty": "沒有已連線的供應商",
"settings.providers.section.popular": "熱門供應商",
+ "settings.providers.search.placeholder": "搜尋供應商",
+ "settings.providers.select.placeholder": "選擇供應商...",
+ "settings.providers.tag.gateway": "Gateway",
"settings.providers.tag.environment": "環境",
"settings.providers.tag.config": "設定",
"settings.providers.tag.custom": "自訂",
"settings.providers.tag.other": "其他",
+ "settings.providers.tag.customProvider": "自訂提供商",
+ "settings.providers.connected.environmentDescription": "從您的環境變數連線",
+ "settings.providers.custom.description": "透過基礎 URL 新增 OpenAI 相容的提供商。",
"settings.providers.modeModels": "按模式選擇模型",
+ "settings.providers.custom.note": "透過 Base URL 新增 OpenAI 相容供應商。",
"settings.providers.modeModels.description": "為特定模式覆寫預設模型。如果未設定,將使用全域預設模型。",
+ "provider.custom.title": "自訂提供商",
+ "provider.custom.description.prefix": "設定 OpenAI 相容的提供商。請參閱",
+ "provider.custom.description.link": "提供商設定文件",
+ "provider.custom.description.suffix": "。",
+ "provider.custom.field.providerID.label": "提供商 ID",
+ "provider.custom.field.providerID.placeholder": "myprovider",
+ "provider.custom.field.providerID.description": "小寫字母、數字、連字號或底線",
+ "provider.custom.field.name.label": "顯示名稱",
+ "provider.custom.field.name.placeholder": "我的 AI 提供商",
+ "provider.custom.field.baseURL.label": "基礎 URL",
+ "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1",
+ "provider.custom.field.apiKey.label": "API 金鑰",
+ "provider.custom.field.apiKey.placeholder": "API 金鑰",
+ "provider.custom.field.apiKey.description": "選填。如果您透過標頭管理驗證,請留空。",
+ "provider.custom.models.label": "模型",
+ "provider.custom.models.id.label": "ID",
+ "provider.custom.models.id.placeholder": "model-id",
+ "provider.custom.models.name.label": "名稱",
+ "provider.custom.models.name.placeholder": "顯示名稱",
+ "provider.custom.models.remove": "移除模型",
+ "provider.custom.models.add": "新增模型",
+ "provider.custom.headers.label": "標頭(選填)",
+ "provider.custom.headers.key.label": "標頭",
+ "provider.custom.headers.key.placeholder": "Header-Name",
+ "provider.custom.headers.value.label": "值",
+ "provider.custom.headers.value.placeholder": "value",
+ "provider.custom.headers.remove": "移除標頭",
+ "provider.custom.headers.add": "新增標頭",
+ "provider.custom.error.providerID.required": "提供商 ID 為必填",
+ "provider.custom.error.providerID.format": "請使用小寫字母、數字、連字號或底線",
+ "provider.custom.error.providerID.exists": "該提供商 ID 已存在",
+ "provider.custom.error.name.required": "顯示名稱為必填",
+ "provider.custom.error.baseURL.required": "基礎 URL 為必填",
+ "provider.custom.error.baseURL.format": "必須以 http:// 或 https:// 開頭",
+ "provider.custom.error.required": "必填",
+ "provider.custom.error.duplicate": "重複",
"settings.models.title": "模型",
"settings.models.description": "可在此調整模型設定。",
"settings.agents.title": "Agents",
@@ -809,6 +854,7 @@ export const dict = {
"deviceAuth.toast.urlCopied": "URL 已複製到剪貼簿",
"deviceAuth.toast.codeCopied": "驗證碼已複製到剪貼簿",
+ "deviceAuth.toast.errorCopied": "錯誤已複製到剪貼簿",
"deviceAuth.status.initiating": "正在啟動登入...",
"deviceAuth.title": "登入 Kilo Code",
"deviceAuth.step1": "步驟 1:開啟此 URL",
@@ -820,8 +866,11 @@ export const dict = {
"deviceAuth.status.waiting": "等待授權中...",
"deviceAuth.status.success": "登入成功!",
"deviceAuth.status.failed": "登入失敗",
+ "deviceAuth.error.detailsTitle": "登入錯誤詳細資訊",
"deviceAuth.status.cancelled": "登入已取消",
"deviceAuth.action.tryAgain": "重試",
+ "deviceAuth.action.copyError": "複製錯誤",
+ "deviceAuth.action.showDetails": "檢視詳細資訊",
"common.retry": "重試",
"common.refresh": "重新整理",
diff --git a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx
index 3342fc977f..f557c5554b 100644
--- a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx
+++ b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx
@@ -76,6 +76,9 @@ const MockProviderProvider: ParentComponent = (props) => {
defaultSelection: () => ({ providerID: "kilo", modelID: "anthropic/claude-sonnet-4-6" }),
models: () => MOCK_MODELS,
findModel: (sel: any) => _findModel(MOCK_MODELS, sel),
+ authMethods: () => ({}),
+ authStates: () => ({}),
+ isModelValid: () => true,
}
return {props.children}
}
diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat.css b/packages/kilo-vscode/webview-ui/src/styles/chat.css
index 59bce17665..1656efead6 100644
--- a/packages/kilo-vscode/webview-ui/src/styles/chat.css
+++ b/packages/kilo-vscode/webview-ui/src/styles/chat.css
@@ -1157,6 +1157,7 @@
.model-selector-list {
overflow-y: auto;
+ scrollbar-gutter: stable;
max-height: 300px;
padding: 4px 0;
}
@@ -1621,6 +1622,32 @@
gap: 8px;
}
+/* Provider Connect Dialog */
+.provider-connect-body {
+ font-size: 13px;
+ color: var(--text-base);
+}
+
+.provider-connect-code-label {
+ font-size: 12px;
+ color: var(--text-weak-base);
+}
+
+.provider-connect-code {
+ font-family: monospace;
+ padding: 8px 10px;
+ border: 1px solid var(--border-weak-base);
+ border-radius: 8px;
+ margin-top: 6px;
+}
+
+.provider-connect-status {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ font-size: 13px;
+}
+
/* ============================================
Working Indicator
============================================ */
diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts
index d34e6ad86c..25ef689f12 100644
--- a/packages/kilo-vscode/webview-ui/src/types/messages.ts
+++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts
@@ -2,6 +2,8 @@
* Types for extension <-> webview message communication
*/
+import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@kilocode/sdk/v2/client"
+
// Connection states
export type ConnectionState = "connecting" | "connected" | "disconnected" | "error"
@@ -289,6 +291,8 @@ export interface Provider {
id: string
name: string
models: Record
+ source?: "env" | "config" | "custom" | "api"
+ env?: string[]
}
export interface ModelSelection {
@@ -296,6 +300,8 @@ export interface ModelSelection {
modelID: string
}
+export type ProviderAuthState = "api" | "oauth" | "wellknown"
+
// ============================================
// Backend Config Types (mirrored for webview)
// ============================================
@@ -321,6 +327,9 @@ export interface ProviderConfig {
api_key?: string
base_url?: string
models?: Record
+ npm?: string
+ env?: string[]
+ options?: Record
}
export interface McpConfig {
@@ -611,6 +620,8 @@ export interface ProvidersLoadedMessage {
connected: string[]
defaults: Record
defaultSelection: ModelSelection
+ authMethods: Record
+ authStates: Record
}
export interface AgentsLoadedMessage {
@@ -806,6 +817,11 @@ export interface VariantsLoadedMessage {
variants: Record
}
+export interface RecentsLoadedMessage {
+ type: "recentsLoaded"
+ recents: ModelSelection[]
+}
+
export interface BranchInfo {
name: string
isLocal: boolean
@@ -1134,6 +1150,33 @@ export interface RemoveInstalledMarketplaceItemMessage {
mpInstallOptions: InstallMarketplaceItemOptions
}
+export interface ProviderOAuthReadyMessage {
+ type: "providerOAuthReady"
+ requestId: string
+ providerID: string
+ authorization: ProviderAuthAuthorization
+}
+
+export interface ProviderConnectedMessage {
+ type: "providerConnected"
+ requestId: string
+ providerID: string
+}
+
+export interface ProviderDisconnectedMessage {
+ type: "providerDisconnected"
+ requestId: string
+ providerID: string
+}
+
+export interface ProviderActionErrorMessage {
+ type: "providerActionError"
+ requestId: string
+ providerID: string
+ action: "authorize" | "connect" | "disconnect"
+ message: string
+}
+
export type ExtensionMessage =
| ReadyMessage
| ConnectionStateMessage
@@ -1218,6 +1261,11 @@ export type ExtensionMessage =
| MarketplaceDataMessage
| MarketplaceInstallResultMessage
| MarketplaceRemoveResultMessage
+ | ProviderOAuthReadyMessage
+ | ProviderConnectedMessage
+ | ProviderDisconnectedMessage
+ | ProviderActionErrorMessage
+ | RecentsLoadedMessage
// ============================================
// Messages FROM webview TO extension
@@ -1771,6 +1819,51 @@ export interface SetDefaultBaseBranchRequest {
branch?: string
}
+export interface ConnectProviderMessage {
+ type: "connectProvider"
+ requestId: string
+ providerID: string
+ apiKey: string
+}
+
+export interface AuthorizeProviderOAuthMessage {
+ type: "authorizeProviderOAuth"
+ requestId: string
+ providerID: string
+ method: number
+}
+
+export interface CompleteProviderOAuthMessage {
+ type: "completeProviderOAuth"
+ requestId: string
+ providerID: string
+ method: number
+ code?: string
+}
+
+export interface DisconnectProviderMessage {
+ type: "disconnectProvider"
+ requestId: string
+ providerID: string
+}
+
+export interface SaveCustomProviderMessage {
+ type: "saveCustomProvider"
+ requestId: string
+ providerID: string
+ config: ProviderConfig
+ apiKey?: string
+}
+
+export interface PersistRecentsRequest {
+ type: "persistRecents"
+ recents: ModelSelection[]
+}
+
+export interface RequestRecentsMessage {
+ type: "requestRecents"
+}
+
export type WebviewMessage =
| SendMessageRequest
| AbortRequest
@@ -1874,6 +1967,13 @@ export type WebviewMessage =
| FilterMarketplaceItemsMessage
| InstallMarketplaceItemMessage
| RemoveInstalledMarketplaceItemMessage
+ | ConnectProviderMessage
+ | AuthorizeProviderOAuthMessage
+ | CompleteProviderOAuthMessage
+ | DisconnectProviderMessage
+ | SaveCustomProviderMessage
+ | PersistRecentsRequest
+ | RequestRecentsMessage
// ============================================
// VS Code API type
diff --git a/packages/kilo-vscode/webview-ui/src/utils/provider-action.ts b/packages/kilo-vscode/webview-ui/src/utils/provider-action.ts
new file mode 100644
index 0000000000..a7d5a62bb7
--- /dev/null
+++ b/packages/kilo-vscode/webview-ui/src/utils/provider-action.ts
@@ -0,0 +1,91 @@
+import type {
+ AuthorizeProviderOAuthMessage,
+ CompleteProviderOAuthMessage,
+ ConnectProviderMessage,
+ DisconnectProviderMessage,
+ ExtensionMessage,
+ ProviderActionErrorMessage,
+ ProviderConnectedMessage,
+ ProviderDisconnectedMessage,
+ ProviderOAuthReadyMessage,
+ SaveCustomProviderMessage,
+ WebviewMessage,
+} from "../types/messages"
+
+type ProviderRequest =
+ | ConnectProviderMessage
+ | AuthorizeProviderOAuthMessage
+ | CompleteProviderOAuthMessage
+ | DisconnectProviderMessage
+ | SaveCustomProviderMessage
+
+type ProviderRequestInput =
+ | Omit
+ | Omit
+ | Omit
+ | Omit
+ | Omit
+
+type Transport = {
+ postMessage: (message: WebviewMessage) => void
+ onMessage: (handler: (message: ExtensionMessage) => void) => () => void
+}
+
+type Handlers = {
+ onOAuthReady?: (message: ProviderOAuthReadyMessage) => void
+ onConnected?: (message: ProviderConnectedMessage) => void
+ onDisconnected?: (message: ProviderDisconnectedMessage) => void
+ onError?: (message: ProviderActionErrorMessage) => void
+}
+
+export function createProviderAction(vscode: Transport) {
+ const pending = new Map()
+ const unsubscribe = vscode.onMessage((message) => {
+ if (!("requestId" in message)) return
+
+ const item = pending.get(message.requestId)
+ if (!item) return
+ pending.delete(message.requestId)
+
+ if (message.type === "providerOAuthReady") {
+ item.onOAuthReady?.(message)
+ return
+ }
+
+ if (message.type === "providerConnected") {
+ item.onConnected?.(message)
+ return
+ }
+
+ if (message.type === "providerDisconnected") {
+ item.onDisconnected?.(message)
+ return
+ }
+
+ if (message.type === "providerActionError") {
+ item.onError?.(message)
+ }
+ })
+
+ function send(message: ProviderRequestInput, handlers: Handlers = {}) {
+ const requestId = crypto.randomUUID()
+ pending.set(requestId, handlers)
+ vscode.postMessage({ ...message, requestId } as ProviderRequest)
+ return requestId
+ }
+
+ function clear(requestId?: string) {
+ if (requestId) {
+ pending.delete(requestId)
+ return
+ }
+ pending.clear()
+ }
+
+ function dispose() {
+ clear()
+ unsubscribe()
+ }
+
+ return { clear, send, dispose }
+}