diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 136c0408f3b..a337470209b 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -1168,6 +1168,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper ]) this.sendNotificationSettings() this.sendTimelineSetting() + this.postMessage({ type: "extensionDataReady" }) // Start polling worktree diff stats for the sidebar badge this.startStatsPolling() diff --git a/packages/kilo-vscode/webview-ui/src/context/config.tsx b/packages/kilo-vscode/webview-ui/src/context/config.tsx index 015c4934bbf..4f98a1b957c 100644 --- a/packages/kilo-vscode/webview-ui/src/context/config.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/config.tsx @@ -8,7 +8,8 @@ * changes into a single write (which triggers disposeAll on the CLI). */ -import { createContext, useContext, createSignal, onCleanup, ParentComponent, Accessor } from "solid-js" +import { createContext, useContext, createSignal, onCleanup } from "solid-js" +import type { ParentComponent, Accessor } from "solid-js" import { useVSCode } from "./vscode" import type { Config, ExtensionMessage } from "../types/messages" import { deepMerge, stripNulls, resolveConfig } from "../utils/config-utils" @@ -71,25 +72,29 @@ export const ConfigProvider: ParentComponent = (props) => { onCleanup(unsubscribe) - // Request config in case the initial push was missed. - // Retry a few times because the extension's httpClient may - // not be ready yet when the first request arrives. - let retries = 0 - const maxRetries = 5 - const retryMs = 500 - + // Request config immediately; if the extension's httpClient is not yet ready, + // extensionDataReady will fire once initialization completes and we retry once. vscode.postMessage({ type: "requestConfig" }) - const retryTimer = setInterval(() => { - retries++ - if (!loading() || retries >= maxRetries) { - clearInterval(retryTimer) - return + const fallback = setTimeout(() => { + if (loading()) { + vscode.postMessage({ type: "requestConfig" }) } - vscode.postMessage({ type: "requestConfig" }) - }, retryMs) + }, 3000) - onCleanup(() => clearInterval(retryTimer)) + const unsubReady = vscode.onMessage((message: ExtensionMessage) => { + if (message.type !== "extensionDataReady") return + unsubReady() + clearTimeout(fallback) + if (loading()) { + vscode.postMessage({ type: "requestConfig" }) + } + }) + + onCleanup(() => { + unsubReady() + clearTimeout(fallback) + }) function updateConfig(partial: Partial) { // Optimistically update local state with deep merge + null stripping diff --git a/packages/kilo-vscode/webview-ui/src/context/provider.tsx b/packages/kilo-vscode/webview-ui/src/context/provider.tsx index f5f95d2d915..4b173b8baa0 100644 --- a/packages/kilo-vscode/webview-ui/src/context/provider.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/provider.tsx @@ -4,7 +4,8 @@ * Selection is now per-session — see session.tsx. */ -import { createContext, useContext, createSignal, createMemo, onCleanup, ParentComponent, Accessor } from "solid-js" +import { createContext, useContext, createSignal, createMemo, onCleanup } from "solid-js" +import type { ParentComponent, Accessor } from "solid-js" import { useVSCode } from "./vscode" import type { Provider, ProviderModel, ModelSelection, ExtensionMessage, ProviderAuthState } from "../types/messages" import type { ProviderAuthMethod } from "@kilocode/sdk/v2/client" @@ -64,25 +65,29 @@ export const ProviderProvider: ParentComponent = (props) => { onCleanup(unsubscribe) - // Request providers in case the initial push was missed. - // Retry a few times because the extension's httpClient may - // not be ready yet when the first request arrives. - let retries = 0 - const maxRetries = 5 - const retryMs = 500 - + // Request providers immediately; if the extension's httpClient is not yet ready, + // extensionDataReady will fire once initialization completes and we retry once. vscode.postMessage({ type: "requestProviders" }) - const retryTimer = setInterval(() => { - retries++ - if (Object.keys(providers()).length > 0 || retries >= maxRetries) { - clearInterval(retryTimer) - return + const fallback = setTimeout(() => { + if (Object.keys(providers()).length === 0) { + vscode.postMessage({ type: "requestProviders" }) } - vscode.postMessage({ type: "requestProviders" }) - }, retryMs) + }, 3000) - onCleanup(() => clearInterval(retryTimer)) + const unsubReady = vscode.onMessage((message: ExtensionMessage) => { + if (message.type !== "extensionDataReady") return + unsubReady() + clearTimeout(fallback) + if (Object.keys(providers()).length === 0) { + vscode.postMessage({ type: "requestProviders" }) + } + }) + + onCleanup(() => { + unsubReady() + clearTimeout(fallback) + }) const value: ProviderContextValue = { providers, diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 4907de27b30..121039e025f 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -503,24 +503,10 @@ export const SessionProvider: ParentComponent = (props) => { }) }) - // Request agents in case the initial push was missed. - // Retry a few times because the extension's httpClient may - // not be ready yet when the first request arrives. - let agentRetries = 0 - const agentMaxRetries = 5 - const agentRetryMs = 500 - + // Request agents immediately; if the extension's httpClient is not yet ready, + // extensionDataReady will fire once initialization completes and we retry once. vscode.postMessage({ type: "requestAgents" }) - const agentRetryTimer = setInterval(() => { - agentRetries++ - if (agents().length > 0 || agentRetries >= agentMaxRetries) { - clearInterval(agentRetryTimer) - return - } - vscode.postMessage({ type: "requestAgents" }) - }, agentRetryMs) - // Skills loaded from the CLI backend const unsubSkills = vscode.onMessage((message: ExtensionMessage) => { if (message.type === "skillsLoaded") { @@ -545,24 +531,28 @@ export const SessionProvider: ParentComponent = (props) => { } }) - // Request MCP status on init with retry (same pattern as agents) - let mcpRetries = 0 + // Request MCP status immediately; retry once on extensionDataReady if still missing. vscode.postMessage({ type: "requestMcpStatus" }) - const mcpRetryTimer = setInterval(() => { - mcpRetries++ - if (Object.keys(mcpStatus()).length > 0 || mcpRetries >= 5) { - clearInterval(mcpRetryTimer) - return - } - vscode.postMessage({ type: "requestMcpStatus" }) - }, 500) + + const fallback = setTimeout(() => { + if (agents().length === 0) vscode.postMessage({ type: "requestAgents" }) + if (Object.keys(mcpStatus()).length === 0) vscode.postMessage({ type: "requestMcpStatus" }) + }, 3000) + + const unsubReady = vscode.onMessage((message: ExtensionMessage) => { + if (message.type !== "extensionDataReady") return + unsubReady() + clearTimeout(fallback) + if (agents().length === 0) vscode.postMessage({ type: "requestAgents" }) + if (Object.keys(mcpStatus()).length === 0) vscode.postMessage({ type: "requestMcpStatus" }) + }) onCleanup(() => { unsubAgents() unsubSkills() unsubMcpStatus() - clearInterval(agentRetryTimer) - clearInterval(mcpRetryTimer) + unsubReady() + clearTimeout(fallback) }) // Variant (thinking effort) selection — keyed by "providerID/modelID" diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index 7961342540f..e783a4eff19 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -1283,6 +1283,10 @@ export interface ClearPendingPromptsMessage { type: "clearPendingPrompts" } +export interface ExtensionDataReadyMessage { + type: "extensionDataReady" +} + // ============================================ // Marketplace Messages // ============================================ @@ -1475,6 +1479,7 @@ export type ExtensionMessage = | WorktreeStatsLoadedMessage | McpStatusLoadedMessage | ClearPendingPromptsMessage + | ExtensionDataReadyMessage // ============================================ // Messages FROM webview TO extension diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 5d97f0939b3..4b143e18e1b 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -149,11 +149,10 @@ export namespace ModelsDev { if (kiloAllowed && !providers["kilo"]) { const kiloOptions = config.provider?.kilo?.options - // kilocode_change start - resolve org ID from auth (OAuth accountId) not just config + // resolve org ID from auth (OAuth accountId) not just config const kiloAuth = await Auth.get("kilo") const kiloOrgId = kiloOptions?.kilocodeOrganizationId ?? (kiloAuth?.type === "oauth" ? kiloAuth.accountId : undefined) - // kilocode_change end const normalizedBaseURL = normalizeKiloBaseURL(kiloOptions?.baseURL, kiloOrgId) const kiloFetchOptions = { ...(normalizedBaseURL ? { baseURL: normalizedBaseURL } : {}), @@ -164,7 +163,19 @@ export namespace ModelsDev { : "https://api.kilo.ai/api/openrouter" const providerBaseURL = normalizedBaseURL ?? defaultBaseURL const ensureTrailingSlash = (value: string): string => (value.endsWith("/") ? value : `${value}/`) - const kiloModels = await ModelCache.fetch("kilo", kiloFetchOptions).catch(() => ({})) + const apertisConfig = config.provider?.apertis?.options + const apertisBaseURL = apertisConfig?.baseURL ?? "https://api.apertis.ai/v1" + const apertisFetchOptions = { + ...(apertisConfig?.baseURL ? { baseURL: apertisConfig.baseURL } : {}), + } + + const [kiloModels, apertisModels] = await Promise.all([ + ModelCache.fetch("kilo", kiloFetchOptions).catch(() => ({})), + !providers["apertis"] + ? ModelCache.fetch("apertis", apertisFetchOptions).catch(() => ({})) + : Promise.resolve(null), + ]) + providers["kilo"] = { id: "kilo", name: "Kilo Gateway", @@ -176,12 +187,22 @@ export namespace ModelsDev { if (Object.keys(kiloModels).length === 0) { ModelCache.refresh("kilo", kiloFetchOptions).catch(() => {}) } - } - // Inject Apertis provider with dynamic model fetching - if (!providers["apertis"]) { - const apertisConfigObj = await Config.get() - const apertisConfig = apertisConfigObj.provider?.apertis?.options + if (!providers["apertis"] && apertisModels !== null) { + providers["apertis"] = { + id: "apertis", + name: "Apertis", + env: ["APERTIS_API_KEY"], + api: apertisBaseURL, + npm: "@ai-sdk/openai-compatible", + models: apertisModels, + } + if (Object.keys(apertisModels).length === 0) { + ModelCache.refresh("apertis", apertisFetchOptions).catch(() => {}) + } + } + } else if (!providers["apertis"]) { + const apertisConfig = config.provider?.apertis?.options const apertisBaseURL = apertisConfig?.baseURL ?? "https://api.apertis.ai/v1" const apertisFetchOptions = { ...(apertisConfig?.baseURL ? { baseURL: apertisConfig.baseURL } : {}),