From ff406f4c172f81a19e155fd3edeb8ee2ad1e63bc Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 12 May 2026 10:17:14 +0200 Subject: [PATCH 01/11] fix(vscode): show model select for Kilo indexing provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Kilo embedding model catalog is Cloud-managed; the indexing settings panel should pick from a Select, never a free-text input. Hide the generic 'Embedding model' TextField and 'Dimension' row when the provider is Kilo, and keep the Select rendered (disabled, 'Loading models…') while the catalog is empty so users never see a freetext fallback. Also harden the catalog request path so transient failures don't poison the UI: the webview retries while the catalog stays empty (mirrors the indexing.tsx pattern), and the extension only caches non-empty catalogs and replays the last good one before re-fetching. --- .changeset/indexing-kilo-model-select.md | 5 + packages/kilo-vscode/src/KiloProvider.ts | 23 +- .../indexing-tab-kilo-no-textfield.test.ts | 64 ++++++ .../unit/kilo-embedding-models-retry.test.ts | 204 ++++++++++++++++++ ...lo-provider-embedding-models-cache.test.ts | 134 ++++++++++++ .../src/components/settings/IndexingTab.tsx | 85 ++++---- .../src/context/kilo-embedding-models.tsx | 76 ++++++- 7 files changed, 542 insertions(+), 49 deletions(-) create mode 100644 .changeset/indexing-kilo-model-select.md create mode 100644 packages/kilo-vscode/tests/unit/indexing-tab-kilo-no-textfield.test.ts create mode 100644 packages/kilo-vscode/tests/unit/kilo-embedding-models-retry.test.ts create mode 100644 packages/kilo-vscode/tests/unit/kilo-provider-embedding-models-cache.test.ts diff --git a/.changeset/indexing-kilo-model-select.md b/.changeset/indexing-kilo-model-select.md new file mode 100644 index 00000000000..ee1519505f8 --- /dev/null +++ b/.changeset/indexing-kilo-model-select.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix the codebase indexing settings to show a model dropdown for the Kilo provider instead of a free-text "Embedding model" input. The Kilo embedding catalog is server-managed, so users should pick from the list rather than typing model ids by hand. While the catalog is loading the dropdown shows "Loading models…" and stays disabled instead of falling back to a placeholder text field. diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index cf3c33479e2..987d1188b0d 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -2152,10 +2152,29 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } private async fetchAndSendKiloEmbeddingModels(): Promise { + // Serve a previously-cached non-empty catalog immediately so the webview + // never regresses to the empty fallback if a fresh fetch fails. + if (this.cachedKiloEmbeddingModelsMessage) { + this.postMessage(this.cachedKiloEmbeddingModelsMessage) + } const catalog = await fetchKiloEmbeddingModelCatalog() const message = { type: "kiloEmbeddingModelsLoaded", catalog } - this.cachedKiloEmbeddingModelsMessage = message - this.postMessage(message) + // Only cache when we got a real catalog. Caching an empty result poisons + // the cache and causes the webview to render the "provider/model" + // placeholder until a full reload, even though the next fetch would + // succeed. Webview-side retries will re-trigger this method until a real + // catalog arrives. + if (catalog.defaultModel && catalog.models.length > 0) { + this.cachedKiloEmbeddingModelsMessage = message + this.postMessage(message) + return + } + // No cache yet: still post the empty result so the webview can decide to + // retry. If we already had a non-empty cache, we already posted it above + // and don't want to clobber it with an empty payload. + if (!this.cachedKiloEmbeddingModelsMessage) { + this.postMessage(message) + } } /** diff --git a/packages/kilo-vscode/tests/unit/indexing-tab-kilo-no-textfield.test.ts b/packages/kilo-vscode/tests/unit/indexing-tab-kilo-no-textfield.test.ts new file mode 100644 index 00000000000..261c9f6503d --- /dev/null +++ b/packages/kilo-vscode/tests/unit/indexing-tab-kilo-no-textfield.test.ts @@ -0,0 +1,64 @@ +/** + * Regression: when provider is `kilo`, the IndexingTab must NOT show a + * free-text "Embedding model" override or a "Dimension" field. The Kilo + * embedding catalog is Cloud-managed: users pick a model from the Select + * (which determines the dimension server-side). A free-text TextField + * leaked through previously and rendered the literal placeholder + * "provider/model" whenever the catalog hadn't loaded yet. + * + * Source-level assertions (rather than full Solid render) keep this test + * cheap and stable across UI library upgrades. + */ + +import { describe, expect, it } from "bun:test" +import fs from "node:fs" +import path from "node:path" + +const SOURCE = fs.readFileSync( + path.resolve(import.meta.dir, "../../webview-ui/src/components/settings/IndexingTab.tsx"), + "utf8", +) + +describe("IndexingTab kilo branch", () => { + it("does not contain the legacy 'provider/model' placeholder fallback", () => { + expect(SOURCE).not.toContain('"provider/model"') + }) + + it("renders the Kilo model preset Select inside the kilo branch", () => { + // The Select for the catalog must reference kiloModels(). The kilo branch + // is the body of , so the + // Select must appear after that line. + const showIdx = SOURCE.indexOf('selectedProvider() === "kilo"') + expect(showIdx).toBeGreaterThan(-1) + + const after = SOURCE.slice(showIdx) + expect(after).toContain("options={kiloModels()}") + expect(after).toContain("settings.indexing.kiloModel.title") + }) + + it("places the free-text Embedding model TextField in the non-kilo fallback only", () => { + // The "Embedding model" row uses settings.indexing.model.title. It must + // appear only inside the `fallback={...}` of the kilo Show, not in the + // main branch. + const fallbackIdx = SOURCE.indexOf("fallback={") + expect(fallbackIdx).toBeGreaterThan(-1) + + const fallbackEnd = SOURCE.indexOf("}\n >", fallbackIdx) + expect(fallbackEnd).toBeGreaterThan(fallbackIdx) + + const fallbackBlock = SOURCE.slice(fallbackIdx, fallbackEnd) + expect(fallbackBlock).toContain("settings.indexing.model.title") + expect(fallbackBlock).toContain("settings.indexing.dimension.title") + expect(fallbackBlock).toContain("text-embedding-3-small") + + // The kilo branch (after the fallback) must NOT reference the override + // model or dimension rows. + const kiloBranch = SOURCE.slice(fallbackEnd) + expect(kiloBranch).not.toContain("settings.indexing.model.title") + expect(kiloBranch).not.toContain("settings.indexing.dimension.title") + }) + + it("disables the kilo Select while the catalog is empty so users cannot select stale state", () => { + expect(SOURCE).toContain("disabled={kiloModels().length === 0}") + }) +}) diff --git a/packages/kilo-vscode/tests/unit/kilo-embedding-models-retry.test.ts b/packages/kilo-vscode/tests/unit/kilo-embedding-models-retry.test.ts new file mode 100644 index 00000000000..4bdb2fce145 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/kilo-embedding-models-retry.test.ts @@ -0,0 +1,204 @@ +/** + * Regression: when the Kilo gateway returns an empty embedding-model catalog + * (network/auth race on first webview boot), the IndexingTab fell back to the + * literal "provider/model" placeholder until a full webview reload. The fix + * is two-layered: + * + * 1. Webview-side retry: re-request the catalog while it stays empty, + * mirroring the indexing.tsx retry shape. + * 2. Extension-side: never replace a non-empty catalog with an empty one. + * + * These tests cover both layers via the pure helpers exposed for testing. + */ + +import { describe, expect, it } from "bun:test" +import { + EMPTY_KILO_EMBEDDING_MODEL_CATALOG, + type KiloEmbeddingModelCatalog, +} from "@kilocode/kilo-indexing/embedding-models" +import { + isEmptyKiloEmbeddingCatalog, + KILO_EMBEDDING_MAX_RETRIES, + subscribeKiloEmbeddingModels, +} from "../../webview-ui/src/context/kilo-embedding-models" +import type { ExtensionMessage, WebviewMessage } from "../../webview-ui/src/types/messages" + +const realCatalog: KiloEmbeddingModelCatalog = { + defaultModel: "kilo/code-embedding-4k", + models: [{ id: "kilo/code-embedding-4k", name: "Code Embedding 4k", dimension: 1024, scoreThreshold: 0.4 }], + aliases: {}, +} + +type FakeTimer = { fn: () => void; ms: number } + +function createFakeTimers() { + const timers = new Map() + let next = 1 + const setIntervalFn = ((fn: () => void, ms: number) => { + const id = next++ + timers.set(id, { fn, ms }) + return id as unknown as ReturnType + }) as typeof setInterval + const clearIntervalFn = ((id: ReturnType) => { + timers.delete(id as unknown as number) + }) as typeof clearInterval + return { + setIntervalFn, + clearIntervalFn, + tick: () => { + // Snapshot to avoid mutation during iteration. + for (const t of [...timers.values()]) t.fn() + }, + pending: () => timers.size, + } +} + +function createHarness() { + const posted: WebviewMessage[] = [] + const handlers = new Set<(m: ExtensionMessage) => void>() + let stored: KiloEmbeddingModelCatalog = EMPTY_KILO_EMBEDDING_MODEL_CATALOG + return { + posted, + deliver: (m: ExtensionMessage) => handlers.forEach((h) => h(m)), + getCatalog: () => stored, + setCatalog: (next: KiloEmbeddingModelCatalog) => { + stored = next + }, + postMessage: (m: WebviewMessage) => { + posted.push(m) + }, + onMessage: (h: (m: ExtensionMessage) => void) => { + handlers.add(h) + return () => handlers.delete(h) + }, + } +} + +describe("isEmptyKiloEmbeddingCatalog", () => { + it("treats EMPTY_KILO_EMBEDDING_MODEL_CATALOG as empty", () => { + expect(isEmptyKiloEmbeddingCatalog(EMPTY_KILO_EMBEDDING_MODEL_CATALOG)).toBe(true) + }) + + it("treats a defaultModel-only payload as empty (models[] missing)", () => { + expect(isEmptyKiloEmbeddingCatalog({ defaultModel: "x", models: [], aliases: {} })).toBe(true) + }) + + it("treats a catalog with models but no defaultModel as empty", () => { + expect( + isEmptyKiloEmbeddingCatalog({ + defaultModel: "", + models: realCatalog.models, + aliases: {}, + }), + ).toBe(true) + }) + + it("recognises a real catalog as non-empty", () => { + expect(isEmptyKiloEmbeddingCatalog(realCatalog)).toBe(false) + }) +}) + +describe("subscribeKiloEmbeddingModels (webview retry)", () => { + it("posts the initial request immediately on subscribe", () => { + const h = createHarness() + const timers = createFakeTimers() + const cleanup = subscribeKiloEmbeddingModels({ + ...h, + setInterval: timers.setIntervalFn, + clearInterval: timers.clearIntervalFn, + }) + + expect(h.posted).toEqual([{ type: "requestKiloEmbeddingModels" }]) + cleanup() + }) + + it("retries while the catalog stays empty", () => { + const h = createHarness() + const timers = createFakeTimers() + const cleanup = subscribeKiloEmbeddingModels({ + ...h, + setInterval: timers.setIntervalFn, + clearInterval: timers.clearIntervalFn, + }) + + // Empty catalog "received" — should keep retrying. + h.deliver({ type: "kiloEmbeddingModelsLoaded", catalog: EMPTY_KILO_EMBEDDING_MODEL_CATALOG }) + + timers.tick() + timers.tick() + + expect(h.posted.length).toBe(3) // initial + 2 retries + cleanup() + }) + + it("stops retrying once a non-empty catalog arrives", () => { + const h = createHarness() + const timers = createFakeTimers() + const cleanup = subscribeKiloEmbeddingModels({ + ...h, + setInterval: timers.setIntervalFn, + clearInterval: timers.clearIntervalFn, + }) + + h.deliver({ type: "kiloEmbeddingModelsLoaded", catalog: realCatalog }) + + timers.tick() + timers.tick() + timers.tick() + + // Only the initial request — retries must short-circuit once we have a catalog. + expect(h.posted.length).toBe(1) + expect(h.getCatalog()).toEqual(realCatalog) + cleanup() + }) + + it("does not exceed the retry cap", () => { + const h = createHarness() + const timers = createFakeTimers() + const cleanup = subscribeKiloEmbeddingModels({ + ...h, + setInterval: timers.setIntervalFn, + clearInterval: timers.clearIntervalFn, + }) + + for (let i = 0; i < KILO_EMBEDDING_MAX_RETRIES + 5; i++) timers.tick() + + // Initial request + at most MAX_RETRIES re-posts. + expect(h.posted.length).toBeLessThanOrEqual(1 + KILO_EMBEDDING_MAX_RETRIES) + cleanup() + }) + + it("ignores empty catalogs delivered after a non-empty one (no regression to placeholder)", () => { + const h = createHarness() + const timers = createFakeTimers() + const cleanup = subscribeKiloEmbeddingModels({ + ...h, + setInterval: timers.setIntervalFn, + clearInterval: timers.clearIntervalFn, + }) + + h.deliver({ type: "kiloEmbeddingModelsLoaded", catalog: realCatalog }) + expect(h.getCatalog()).toEqual(realCatalog) + + // Late empty payload (e.g. another extension push). Must NOT clobber the + // good catalog or IndexingTab will fall back to "provider/model". + h.deliver({ type: "kiloEmbeddingModelsLoaded", catalog: EMPTY_KILO_EMBEDDING_MODEL_CATALOG }) + + expect(h.getCatalog()).toEqual(realCatalog) + cleanup() + }) + + it("clears the retry timer on cleanup", () => { + const h = createHarness() + const timers = createFakeTimers() + const cleanup = subscribeKiloEmbeddingModels({ + ...h, + setInterval: timers.setIntervalFn, + clearInterval: timers.clearIntervalFn, + }) + + expect(timers.pending()).toBe(1) + cleanup() + expect(timers.pending()).toBe(0) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-embedding-models-cache.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-embedding-models-cache.test.ts new file mode 100644 index 00000000000..99f39ff0a6a --- /dev/null +++ b/packages/kilo-vscode/tests/unit/kilo-provider-embedding-models-cache.test.ts @@ -0,0 +1,134 @@ +/** + * Regression: KiloProvider used to cache and post any catalog returned by the + * gateway, including the empty fallback that `fetchKiloEmbeddingModelCatalog` + * returns on transient failures. That poisoned the cache so subsequent + * `requestKiloEmbeddingModels` calls replayed the empty payload and + * IndexingTab kept showing the literal "provider/model" placeholder until a + * full webview reload. + * + * The fix: + * - Never cache an empty catalog. + * - When a non-empty catalog is already cached, replay it before re-fetching + * so a transient failure cannot regress the UI. + */ + +import { describe, expect, it } from "bun:test" + +// vscode mock is provided by the shared preload (tests/setup/vscode-mock.ts) +const { KiloProvider } = await import("../../src/KiloProvider") + +type Internals = { + webview: { postMessage: (message: unknown) => Promise } | null + cachedKiloEmbeddingModelsMessage: unknown + fetchAndSendKiloEmbeddingModels: () => Promise +} + +type Catalog = { + defaultModel: string + models: Array<{ id: string; name: string; dimension: number; scoreThreshold: number }> + aliases: Record +} + +const REAL: Catalog = { + defaultModel: "kilo/code-embedding-4k", + models: [{ id: "kilo/code-embedding-4k", name: "Code Embedding 4k", dimension: 1024, scoreThreshold: 0.4 }], + aliases: {}, +} + +const EMPTY: Catalog = { defaultModel: "", models: [], aliases: {} } + +function createProvider() { + const sent: Array<{ type: string; catalog?: Catalog }> = [] + const provider = new KiloProvider({} as never, {} as never) + const internal = provider as unknown as Internals + internal.webview = { + postMessage: async (message) => { + sent.push(message as { type: string; catalog?: Catalog }) + return true + }, + } + return { provider, internal, sent } +} + +function mockGatewayFetch(responses: Array) { + const original = globalThis.fetch + let i = 0 + globalThis.fetch = (async () => { + const next = responses[i++] ?? responses[responses.length - 1] + if (next === "fail") return new Response("nope", { status: 500 }) + return new Response(JSON.stringify(next), { status: 200, headers: { "content-type": "application/json" } }) + }) as typeof fetch + return () => { + globalThis.fetch = original + } +} + +describe("KiloProvider.fetchAndSendKiloEmbeddingModels", () => { + it("caches a real catalog and posts it to the webview", async () => { + const { internal, sent } = createProvider() + const restore = mockGatewayFetch([REAL]) + try { + await internal.fetchAndSendKiloEmbeddingModels() + } finally { + restore() + } + + expect(sent.length).toBe(1) + expect(sent[0]?.type).toBe("kiloEmbeddingModelsLoaded") + expect(sent[0]?.catalog).toEqual(REAL) + expect(internal.cachedKiloEmbeddingModelsMessage).toEqual({ type: "kiloEmbeddingModelsLoaded", catalog: REAL }) + }) + + it("does NOT cache an empty catalog (transient failure must not poison the cache)", async () => { + const { internal, sent } = createProvider() + const restore = mockGatewayFetch(["fail"]) + try { + await internal.fetchAndSendKiloEmbeddingModels() + } finally { + restore() + } + + // Empty payload still posted so the webview can decide to retry. + expect(sent.length).toBe(1) + expect(sent[0]?.catalog).toEqual(EMPTY) + // ...but the cache must stay null so the next request triggers a fresh + // fetch instead of replaying the empty response. + expect(internal.cachedKiloEmbeddingModelsMessage).toBeNull() + }) + + it("recovers on retry: empty fetch followed by a real fetch produces a cached real catalog", async () => { + const { internal, sent } = createProvider() + const restore = mockGatewayFetch(["fail", REAL]) + try { + await internal.fetchAndSendKiloEmbeddingModels() + await internal.fetchAndSendKiloEmbeddingModels() + } finally { + restore() + } + + // First call posts empty; second call posts real. + expect(sent.length).toBe(2) + expect(sent[0]?.catalog).toEqual(EMPTY) + expect(sent[1]?.catalog).toEqual(REAL) + expect(internal.cachedKiloEmbeddingModelsMessage).toEqual({ type: "kiloEmbeddingModelsLoaded", catalog: REAL }) + }) + + it("replays the cached real catalog before re-fetching, so a later transient failure cannot regress", async () => { + const { internal, sent } = createProvider() + const restore = mockGatewayFetch([REAL, "fail"]) + try { + await internal.fetchAndSendKiloEmbeddingModels() + sent.length = 0 // reset to inspect the second call only + await internal.fetchAndSendKiloEmbeddingModels() + } finally { + restore() + } + + // Second call: cached real catalog is replayed first. The fresh fetch + // returns empty but must NOT be posted (would clobber the good catalog + // in webview state) and must NOT overwrite the cache. + expect(sent.length).toBe(1) + expect(sent[0]?.catalog).toEqual(REAL) + expect(internal.cachedKiloEmbeddingModelsMessage).toEqual({ type: "kiloEmbeddingModelsLoaded", catalog: REAL }) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx index 60cd1199fbc..0d4b0ff3eee 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx @@ -264,47 +264,52 @@ const IndexingTab: Component = () => { placeholder={language.t("settings.providers.notSet")} /> - - 0}> - - item.value === knownKiloModel(cfg().model))} + value={(item) => item.value} + label={(item) => item.label} + onSelect={(item) => updateIndexing({ model: item?.value ?? kiloDefault(), dimension: undefined })} + variant="secondary" + size="small" + triggerVariant="settings" + disabled={kiloModels().length === 0} + placeholder={kiloModels().length === 0 ? "Loading models…" : "Custom model"} + /> + + @@ -12,18 +12,80 @@ type KiloEmbeddingModelsContextValue = { export const KiloEmbeddingModelsContext = createContext() +// Retry while the catalog stays empty. The gateway request can race with +// network/auth readiness on first webview boot; without retries a single +// empty response leaves IndexingTab showing the literal "provider/model" +// placeholder until a full webview reload. Mirrors the retry shape used by +// indexing.tsx (5 attempts, 500ms apart). +export const KILO_EMBEDDING_MAX_RETRIES = 5 +export const KILO_EMBEDDING_RETRY_MS = 500 + +export const isEmptyKiloEmbeddingCatalog = (cat: KiloEmbeddingModelCatalog) => + !cat.defaultModel || cat.models.length === 0 + +type SubscribeOptions = { + postMessage: (message: WebviewMessage) => void + onMessage: (handler: (message: ExtensionMessage) => void) => () => void + getCatalog: () => KiloEmbeddingModelCatalog + setCatalog: (next: KiloEmbeddingModelCatalog) => void + setInterval?: typeof globalThis.setInterval + clearInterval?: typeof globalThis.clearInterval + maxRetries?: number + retryMs?: number +} + +/** + * Pure orchestration: subscribes to `kiloEmbeddingModelsLoaded` messages, + * sends the initial request, and retries while the catalog is empty. Returned + * function cleans up the subscription and timer. + * + * Extracted from the SolidJS provider so the retry/empty-catalog behaviour can + * be unit-tested without spinning up a webview. + */ +export function subscribeKiloEmbeddingModels(opts: SubscribeOptions): () => void { + const setIntervalFn = opts.setInterval ?? globalThis.setInterval + const clearIntervalFn = opts.clearInterval ?? globalThis.clearInterval + const maxRetries = opts.maxRetries ?? KILO_EMBEDDING_MAX_RETRIES + const retryMs = opts.retryMs ?? KILO_EMBEDDING_RETRY_MS + + const unsubscribe = opts.onMessage((message: ExtensionMessage) => { + if (message.type !== "kiloEmbeddingModelsLoaded") return + // Never let an empty catalog overwrite a non-empty one that arrived + // earlier (e.g. cached message replayed after a transient failure). + if (isEmptyKiloEmbeddingCatalog(message.catalog) && !isEmptyKiloEmbeddingCatalog(opts.getCatalog())) return + opts.setCatalog(message.catalog) + }) + + opts.postMessage({ type: "requestKiloEmbeddingModels" }) + + let retries = 0 + const timer = setIntervalFn(() => { + retries++ + if (!isEmptyKiloEmbeddingCatalog(opts.getCatalog()) || retries >= maxRetries) { + clearIntervalFn(timer) + return + } + opts.postMessage({ type: "requestKiloEmbeddingModels" }) + }, retryMs) + + return () => { + clearIntervalFn(timer) + unsubscribe() + } +} + export const KiloEmbeddingModelsProvider: ParentComponent = (props) => { const vscode = useVSCode() const [catalog, setCatalog] = createSignal(EMPTY_KILO_EMBEDDING_MODEL_CATALOG) - const unsubscribe = vscode.onMessage((message: ExtensionMessage) => { - if (message.type !== "kiloEmbeddingModelsLoaded") return - setCatalog(message.catalog) + const cleanup = subscribeKiloEmbeddingModels({ + postMessage: vscode.postMessage, + onMessage: vscode.onMessage, + getCatalog: catalog, + setCatalog, }) - vscode.postMessage({ type: "requestKiloEmbeddingModels" }) - - onCleanup(unsubscribe) + onCleanup(cleanup) return {props.children} } From 0ab5ae9a8628eb6fad9b773a3f43a51d1dc87f38 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 12 May 2026 11:08:23 +0200 Subject: [PATCH 02/11] fix(indexing): avoid Kilo embedding dimension override Kilo-hosted embedding models use Cloud catalog dimensions for vector-store setup, but the embedding request itself should not forward a dimensions override through the gateway. Some upstream embedding APIs reject that parameter even when it matches the native dimension. Also move the Kilo embedding catalog retry helper into a JSX-free module so unit tests can import it in CI without requiring the Solid JSX runtime, and tighten the retry-cap assertion. --- .changeset/kilo-indexing-native-dimensions.md | 5 ++ .../src/indexing/embedders/kilo.ts | 3 +- .../src/indexing/service-factory.ts | 1 - .../kilocode/indexing/embedders/kilo.test.ts | 21 ++++++ .../unit/kilo-embedding-models-retry.test.ts | 6 +- .../kilo-embedding-models-subscribe.ts | 64 +++++++++++++++++++ .../src/context/kilo-embedding-models.tsx | 64 +------------------ 7 files changed, 95 insertions(+), 69 deletions(-) create mode 100644 .changeset/kilo-indexing-native-dimensions.md create mode 100644 packages/kilo-vscode/webview-ui/src/context/kilo-embedding-models-subscribe.ts diff --git a/.changeset/kilo-indexing-native-dimensions.md b/.changeset/kilo-indexing-native-dimensions.md new file mode 100644 index 00000000000..24489ea6b87 --- /dev/null +++ b/.changeset/kilo-indexing-native-dimensions.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-indexing": patch +--- + +Stop sending dimensions overrides through the Kilo embedding provider. Kilo-hosted models use their native catalog dimensions for vector store setup, and the gateway should not receive provider-specific dimension overrides that some upstream embedding APIs reject. diff --git a/packages/kilo-indexing/src/indexing/embedders/kilo.ts b/packages/kilo-indexing/src/indexing/embedders/kilo.ts index 0627f2f1fd6..e101f73176e 100644 --- a/packages/kilo-indexing/src/indexing/embedders/kilo.ts +++ b/packages/kilo-indexing/src/indexing/embedders/kilo.ts @@ -18,7 +18,6 @@ export class KiloEmbedder implements IEmbedder { baseUrl?: string organizationId?: string modelId?: string - dimensions?: number }) { if (!input.apiKey) throw new Error("Kilo API key is required for embedding.") @@ -34,7 +33,7 @@ export class KiloEmbedder implements IEmbedder { input.apiKey, this.model, MAX_ITEM_TOKENS, - { headers, dimensions: input.dimensions }, + { headers }, ) } diff --git a/packages/kilo-indexing/src/indexing/service-factory.ts b/packages/kilo-indexing/src/indexing/service-factory.ts index aa8f14eef7e..7b18eec4263 100644 --- a/packages/kilo-indexing/src/indexing/service-factory.ts +++ b/packages/kilo-indexing/src/indexing/service-factory.ts @@ -72,7 +72,6 @@ export class CodeIndexServiceFactory { baseUrl: config.kiloOptions.baseUrl, organizationId: config.kiloOptions.organizationId, modelId: config.modelId, - dimensions: config.modelDimension, }) } if (provider === "openai") { diff --git a/packages/kilo-indexing/test/kilocode/indexing/embedders/kilo.test.ts b/packages/kilo-indexing/test/kilocode/indexing/embedders/kilo.test.ts index abcbc066bcb..359982ee689 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/embedders/kilo.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/embedders/kilo.test.ts @@ -42,6 +42,27 @@ describe("KiloEmbedder", () => { }) }) + test("does not forward dimensions to the gateway", async () => { + mockEmbeddingsCreate.mockResolvedValue({ + data: [{ embedding: [0.1, 0.2] }], + usage: { prompt_tokens: 1, total_tokens: 1 }, + }) + + const embedder = new KiloEmbedder({ + apiKey: "kilo-token", + organizationId: "org_123", + modelId: "mistralai/mistral-embed-2312", + }) + + await embedder.createEmbeddings(["hello"]) + + expect(mockEmbeddingsCreate).toHaveBeenCalledWith({ + input: ["hello"], + model: "mistralai/mistral-embed-2312", + encoding_format: "base64", + }) + }) + test("normalizes custom gateway base URLs", () => { const seen: unknown[] = [] setOpenAIConstructorHook((cfg) => seen.push(cfg)) diff --git a/packages/kilo-vscode/tests/unit/kilo-embedding-models-retry.test.ts b/packages/kilo-vscode/tests/unit/kilo-embedding-models-retry.test.ts index 4bdb2fce145..1f9510c0bd8 100644 --- a/packages/kilo-vscode/tests/unit/kilo-embedding-models-retry.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-embedding-models-retry.test.ts @@ -20,7 +20,7 @@ import { isEmptyKiloEmbeddingCatalog, KILO_EMBEDDING_MAX_RETRIES, subscribeKiloEmbeddingModels, -} from "../../webview-ui/src/context/kilo-embedding-models" +} from "../../webview-ui/src/context/kilo-embedding-models-subscribe" import type { ExtensionMessage, WebviewMessage } from "../../webview-ui/src/types/messages" const realCatalog: KiloEmbeddingModelCatalog = { @@ -163,8 +163,8 @@ describe("subscribeKiloEmbeddingModels (webview retry)", () => { for (let i = 0; i < KILO_EMBEDDING_MAX_RETRIES + 5; i++) timers.tick() - // Initial request + at most MAX_RETRIES re-posts. - expect(h.posted.length).toBeLessThanOrEqual(1 + KILO_EMBEDDING_MAX_RETRIES) + // Initial request + exactly MAX_RETRIES re-posts. + expect(h.posted.length).toBe(1 + KILO_EMBEDDING_MAX_RETRIES) cleanup() }) diff --git a/packages/kilo-vscode/webview-ui/src/context/kilo-embedding-models-subscribe.ts b/packages/kilo-vscode/webview-ui/src/context/kilo-embedding-models-subscribe.ts new file mode 100644 index 00000000000..e1bf2acddfb --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/context/kilo-embedding-models-subscribe.ts @@ -0,0 +1,64 @@ +import type { KiloEmbeddingModelCatalog } from "@kilocode/kilo-indexing/embedding-models" +import type { ExtensionMessage, WebviewMessage } from "../types/messages" + +// Retry while the catalog stays empty. The gateway request can race with +// network/auth readiness on first webview boot; without retries a single +// empty response leaves IndexingTab in its loading state until a full +// webview reload. Sends up to 5 retry requests, 500ms apart. +export const KILO_EMBEDDING_MAX_RETRIES = 5 +export const KILO_EMBEDDING_RETRY_MS = 500 + +export const isEmptyKiloEmbeddingCatalog = (cat: KiloEmbeddingModelCatalog) => + !cat.defaultModel || cat.models.length === 0 + +type SubscribeOptions = { + postMessage: (message: WebviewMessage) => void + onMessage: (handler: (message: ExtensionMessage) => void) => () => void + getCatalog: () => KiloEmbeddingModelCatalog + setCatalog: (next: KiloEmbeddingModelCatalog) => void + setInterval?: typeof globalThis.setInterval + clearInterval?: typeof globalThis.clearInterval + maxRetries?: number + retryMs?: number +} + +/** + * Pure orchestration: subscribes to `kiloEmbeddingModelsLoaded` messages, + * sends the initial request, and retries while the catalog is empty. Returned + * function cleans up the subscription and timer. + * + * Lives in a JSX-free module so unit tests can import it without pulling in + * the SolidJS render pipeline (the `.tsx` provider also re-exports it). + */ +export function subscribeKiloEmbeddingModels(opts: SubscribeOptions): () => void { + const setIntervalFn = opts.setInterval ?? globalThis.setInterval + const clearIntervalFn = opts.clearInterval ?? globalThis.clearInterval + const maxRetries = opts.maxRetries ?? KILO_EMBEDDING_MAX_RETRIES + const retryMs = opts.retryMs ?? KILO_EMBEDDING_RETRY_MS + + const unsubscribe = opts.onMessage((message: ExtensionMessage) => { + if (message.type !== "kiloEmbeddingModelsLoaded") return + // Never let an empty catalog overwrite a non-empty one that arrived + // earlier (e.g. cached message replayed after a transient failure). + if (isEmptyKiloEmbeddingCatalog(message.catalog) && !isEmptyKiloEmbeddingCatalog(opts.getCatalog())) return + opts.setCatalog(message.catalog) + }) + + opts.postMessage({ type: "requestKiloEmbeddingModels" }) + + let retries = 0 + const timer = setIntervalFn(() => { + if (!isEmptyKiloEmbeddingCatalog(opts.getCatalog()) || retries >= maxRetries) { + clearIntervalFn(timer) + return + } + retries++ + opts.postMessage({ type: "requestKiloEmbeddingModels" }) + if (retries >= maxRetries) clearIntervalFn(timer) + }, retryMs) + + return () => { + clearIntervalFn(timer) + unsubscribe() + } +} diff --git a/packages/kilo-vscode/webview-ui/src/context/kilo-embedding-models.tsx b/packages/kilo-vscode/webview-ui/src/context/kilo-embedding-models.tsx index 5653af87d0d..eba0663d682 100644 --- a/packages/kilo-vscode/webview-ui/src/context/kilo-embedding-models.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/kilo-embedding-models.tsx @@ -4,7 +4,7 @@ import { type KiloEmbeddingModelCatalog, } from "@kilocode/kilo-indexing/embedding-models" import { useVSCode } from "./vscode" -import type { ExtensionMessage, WebviewMessage } from "../types/messages" +import { subscribeKiloEmbeddingModels } from "./kilo-embedding-models-subscribe" type KiloEmbeddingModelsContextValue = { catalog: Accessor @@ -12,68 +12,6 @@ type KiloEmbeddingModelsContextValue = { export const KiloEmbeddingModelsContext = createContext() -// Retry while the catalog stays empty. The gateway request can race with -// network/auth readiness on first webview boot; without retries a single -// empty response leaves IndexingTab showing the literal "provider/model" -// placeholder until a full webview reload. Mirrors the retry shape used by -// indexing.tsx (5 attempts, 500ms apart). -export const KILO_EMBEDDING_MAX_RETRIES = 5 -export const KILO_EMBEDDING_RETRY_MS = 500 - -export const isEmptyKiloEmbeddingCatalog = (cat: KiloEmbeddingModelCatalog) => - !cat.defaultModel || cat.models.length === 0 - -type SubscribeOptions = { - postMessage: (message: WebviewMessage) => void - onMessage: (handler: (message: ExtensionMessage) => void) => () => void - getCatalog: () => KiloEmbeddingModelCatalog - setCatalog: (next: KiloEmbeddingModelCatalog) => void - setInterval?: typeof globalThis.setInterval - clearInterval?: typeof globalThis.clearInterval - maxRetries?: number - retryMs?: number -} - -/** - * Pure orchestration: subscribes to `kiloEmbeddingModelsLoaded` messages, - * sends the initial request, and retries while the catalog is empty. Returned - * function cleans up the subscription and timer. - * - * Extracted from the SolidJS provider so the retry/empty-catalog behaviour can - * be unit-tested without spinning up a webview. - */ -export function subscribeKiloEmbeddingModels(opts: SubscribeOptions): () => void { - const setIntervalFn = opts.setInterval ?? globalThis.setInterval - const clearIntervalFn = opts.clearInterval ?? globalThis.clearInterval - const maxRetries = opts.maxRetries ?? KILO_EMBEDDING_MAX_RETRIES - const retryMs = opts.retryMs ?? KILO_EMBEDDING_RETRY_MS - - const unsubscribe = opts.onMessage((message: ExtensionMessage) => { - if (message.type !== "kiloEmbeddingModelsLoaded") return - // Never let an empty catalog overwrite a non-empty one that arrived - // earlier (e.g. cached message replayed after a transient failure). - if (isEmptyKiloEmbeddingCatalog(message.catalog) && !isEmptyKiloEmbeddingCatalog(opts.getCatalog())) return - opts.setCatalog(message.catalog) - }) - - opts.postMessage({ type: "requestKiloEmbeddingModels" }) - - let retries = 0 - const timer = setIntervalFn(() => { - retries++ - if (!isEmptyKiloEmbeddingCatalog(opts.getCatalog()) || retries >= maxRetries) { - clearIntervalFn(timer) - return - } - opts.postMessage({ type: "requestKiloEmbeddingModels" }) - }, retryMs) - - return () => { - clearIntervalFn(timer) - unsubscribe() - } -} - export const KiloEmbeddingModelsProvider: ParentComponent = (props) => { const vscode = useVSCode() const [catalog, setCatalog] = createSignal(EMPTY_KILO_EMBEDDING_MODEL_CATALOG) From 15925f0b7e844e6ce99df15289e3a3967c798d5e Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 12 May 2026 11:16:22 +0200 Subject: [PATCH 03/11] fix(vscode): clear stale indexing dimensions for Kilo provider Switching to the Kilo indexing provider must delete any previously configured manual dimension. Kilo still passes configured dimensions through to the embedder when users intentionally set one, but the Kilo model Select uses catalog-native dimensions and should not inherit stale values from another provider. --- .../kilo-indexing/src/indexing/embedders/kilo.ts | 1 + .../kilo-indexing/src/indexing/service-factory.ts | 1 + .../kilo-vscode/tests/unit/config-utils.test.ts | 15 +++++++++++++++ .../unit/indexing-tab-kilo-no-textfield.test.ts | 5 +++++ .../src/components/settings/IndexingTab.tsx | 8 +++++--- .../webview-ui/src/types/messages/config.ts | 2 +- 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/kilo-indexing/src/indexing/embedders/kilo.ts b/packages/kilo-indexing/src/indexing/embedders/kilo.ts index e101f73176e..59be130ec9c 100644 --- a/packages/kilo-indexing/src/indexing/embedders/kilo.ts +++ b/packages/kilo-indexing/src/indexing/embedders/kilo.ts @@ -18,6 +18,7 @@ export class KiloEmbedder implements IEmbedder { baseUrl?: string organizationId?: string modelId?: string + dimensions?: number }) { if (!input.apiKey) throw new Error("Kilo API key is required for embedding.") diff --git a/packages/kilo-indexing/src/indexing/service-factory.ts b/packages/kilo-indexing/src/indexing/service-factory.ts index 7b18eec4263..aa8f14eef7e 100644 --- a/packages/kilo-indexing/src/indexing/service-factory.ts +++ b/packages/kilo-indexing/src/indexing/service-factory.ts @@ -72,6 +72,7 @@ export class CodeIndexServiceFactory { baseUrl: config.kiloOptions.baseUrl, organizationId: config.kiloOptions.organizationId, modelId: config.modelId, + dimensions: config.modelDimension, }) } if (provider === "openai") { diff --git a/packages/kilo-vscode/tests/unit/config-utils.test.ts b/packages/kilo-vscode/tests/unit/config-utils.test.ts index f5842ca032b..f67105eafd7 100644 --- a/packages/kilo-vscode/tests/unit/config-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/config-utils.test.ts @@ -330,6 +330,21 @@ describe("ConfigState", () => { }) }) + describe("clearing indexing dimension for Kilo provider", () => { + it("keeps null in the draft so stale manual dimensions are deleted", () => { + const s = new ConfigState() + s.handleConfigLoaded({ indexing: { provider: "openrouter", model: "openai/text-embedding-3-small", dimension: 256 } }) + + s.updateConfig({ indexing: { provider: "kilo", model: "mistralai/codestral-embed-2505", dimension: null } }) + + expect(s.config.indexing?.dimension).toBeUndefined() + expect(s.draft.indexing?.dimension).toBeNull() + expect(JSON.parse(JSON.stringify(s.draft))).toEqual({ + indexing: { provider: "kilo", model: "mistralai/codestral-embed-2505", dimension: null }, + }) + }) + }) + describe("agent permission patches", () => { it("merges nested per-agent permission patches into existing rules", () => { const s = new ConfigState() diff --git a/packages/kilo-vscode/tests/unit/indexing-tab-kilo-no-textfield.test.ts b/packages/kilo-vscode/tests/unit/indexing-tab-kilo-no-textfield.test.ts index 261c9f6503d..2de3728475a 100644 --- a/packages/kilo-vscode/tests/unit/indexing-tab-kilo-no-textfield.test.ts +++ b/packages/kilo-vscode/tests/unit/indexing-tab-kilo-no-textfield.test.ts @@ -61,4 +61,9 @@ describe("IndexingTab kilo branch", () => { it("disables the kilo Select while the catalog is empty so users cannot select stale state", () => { expect(SOURCE).toContain("disabled={kiloModels().length === 0}") }) + + it("clears stale manual dimensions with null delete sentinels when using Kilo", () => { + expect(SOURCE).toContain("dimension: null") + expect(SOURCE).not.toContain("dimension: undefined })}") + }) }) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx index 0d4b0ff3eee..b7aaf13bb08 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx @@ -117,11 +117,11 @@ const IndexingTab: Component = () => { updateIndexing({ provider: next, model, - dimension: undefined, + dimension: null, }) return } - updateIndexing({ provider: next, model: undefined, dimension: undefined }) + updateIndexing({ provider: next, model: undefined, dimension: null }) } const saveEnabled = (enabled: boolean) => { @@ -130,6 +130,7 @@ const IndexingTab: Component = () => { enabled, provider: "kilo", model: knownKiloModel(cfg().model) ?? kiloDefault(), + dimension: null, }) return } @@ -143,6 +144,7 @@ const IndexingTab: Component = () => { enabled, provider: "kilo", model: knownKiloModel(cfg().model) ?? kiloDefault(), + dimension: null, }, }) return @@ -301,7 +303,7 @@ const IndexingTab: Component = () => { current={kiloModels().find((item) => item.value === knownKiloModel(cfg().model))} value={(item) => item.value} label={(item) => item.label} - onSelect={(item) => updateIndexing({ model: item?.value ?? kiloDefault(), dimension: undefined })} + onSelect={(item) => updateIndexing({ model: item?.value ?? kiloDefault(), dimension: null })} variant="secondary" size="small" triggerVariant="settings" diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts index ca0c91c3d39..9e7c4ee2852 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts @@ -67,7 +67,7 @@ export interface IndexingConfig { enabled?: boolean provider?: IndexingProvider model?: string - dimension?: number + dimension?: number | null vectorStore?: "lancedb" | "qdrant" kilo?: { apiKey?: string; baseUrl?: string; organizationId?: string } openai?: { apiKey?: string } From afa2a65be55860b90a49afe14d0a24fee64d889a Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 12 May 2026 12:04:43 +0200 Subject: [PATCH 04/11] fix(indexing): keep Kilo embedding request dimensions --- .changeset/kilo-indexing-native-dimensions.md | 5 ----- .../src/indexing/embedders/kilo.ts | 2 +- .../kilocode/indexing/embedders/kilo.test.ts | 21 ------------------- 3 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 .changeset/kilo-indexing-native-dimensions.md diff --git a/.changeset/kilo-indexing-native-dimensions.md b/.changeset/kilo-indexing-native-dimensions.md deleted file mode 100644 index 24489ea6b87..00000000000 --- a/.changeset/kilo-indexing-native-dimensions.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-indexing": patch ---- - -Stop sending dimensions overrides through the Kilo embedding provider. Kilo-hosted models use their native catalog dimensions for vector store setup, and the gateway should not receive provider-specific dimension overrides that some upstream embedding APIs reject. diff --git a/packages/kilo-indexing/src/indexing/embedders/kilo.ts b/packages/kilo-indexing/src/indexing/embedders/kilo.ts index 59be130ec9c..0627f2f1fd6 100644 --- a/packages/kilo-indexing/src/indexing/embedders/kilo.ts +++ b/packages/kilo-indexing/src/indexing/embedders/kilo.ts @@ -34,7 +34,7 @@ export class KiloEmbedder implements IEmbedder { input.apiKey, this.model, MAX_ITEM_TOKENS, - { headers }, + { headers, dimensions: input.dimensions }, ) } diff --git a/packages/kilo-indexing/test/kilocode/indexing/embedders/kilo.test.ts b/packages/kilo-indexing/test/kilocode/indexing/embedders/kilo.test.ts index 359982ee689..abcbc066bcb 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/embedders/kilo.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/embedders/kilo.test.ts @@ -42,27 +42,6 @@ describe("KiloEmbedder", () => { }) }) - test("does not forward dimensions to the gateway", async () => { - mockEmbeddingsCreate.mockResolvedValue({ - data: [{ embedding: [0.1, 0.2] }], - usage: { prompt_tokens: 1, total_tokens: 1 }, - }) - - const embedder = new KiloEmbedder({ - apiKey: "kilo-token", - organizationId: "org_123", - modelId: "mistralai/mistral-embed-2312", - }) - - await embedder.createEmbeddings(["hello"]) - - expect(mockEmbeddingsCreate).toHaveBeenCalledWith({ - input: ["hello"], - model: "mistralai/mistral-embed-2312", - encoding_format: "base64", - }) - }) - test("normalizes custom gateway base URLs", () => { const seen: unknown[] = [] setOpenAIConstructorHook((cfg) => seen.push(cfg)) From 9cd4028d3f032ece989750bbd0d7af5409aa9198 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 14:14:24 +0000 Subject: [PATCH 05/11] fix(vscode): remove stale re-export comment in kilo-embedding-models-subscribe --- .../webview-ui/src/context/kilo-embedding-models-subscribe.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/src/context/kilo-embedding-models-subscribe.ts b/packages/kilo-vscode/webview-ui/src/context/kilo-embedding-models-subscribe.ts index e1bf2acddfb..99742bc0d03 100644 --- a/packages/kilo-vscode/webview-ui/src/context/kilo-embedding-models-subscribe.ts +++ b/packages/kilo-vscode/webview-ui/src/context/kilo-embedding-models-subscribe.ts @@ -28,7 +28,7 @@ type SubscribeOptions = { * function cleans up the subscription and timer. * * Lives in a JSX-free module so unit tests can import it without pulling in - * the SolidJS render pipeline (the `.tsx` provider also re-exports it). + * the SolidJS render pipeline. */ export function subscribeKiloEmbeddingModels(opts: SubscribeOptions): () => void { const setIntervalFn = opts.setInterval ?? globalThis.setInterval From db5c3c69bd82badb493c3f3c105dd14a2c85a8c9 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 13 May 2026 09:16:47 +0200 Subject: [PATCH 06/11] fix(indexing): surface embedding provider errors Show structured provider response bodies in Kilo indexing embedder failures instead of collapsing OpenAI SDK errors to generic Bad Request messages. Also avoid sending invalid null indexing dimensions in the settings save payload while keeping Kilo provider selections free of stale manual dimensions in local UI state. --- .changeset/indexing-kilo-model-select.md | 3 ++ .../indexing/embedders/openai-compatible.ts | 47 +++++++++++++++++-- .../embedders/openai-compatible.test.ts | 20 ++++++++ .../tests/unit/config-scope.test.ts | 12 +++++ .../tests/unit/config-utils.test.ts | 15 ------ .../indexing-tab-kilo-no-textfield.test.ts | 7 +-- .../src/components/settings/IndexingTab.tsx | 11 ++--- .../webview-ui/src/types/messages/config.ts | 2 +- .../webview-ui/src/utils/config-scope.ts | 7 +-- 9 files changed, 92 insertions(+), 32 deletions(-) diff --git a/.changeset/indexing-kilo-model-select.md b/.changeset/indexing-kilo-model-select.md index ee1519505f8..9dd27683056 100644 --- a/.changeset/indexing-kilo-model-select.md +++ b/.changeset/indexing-kilo-model-select.md @@ -1,5 +1,8 @@ --- "kilo-code": patch +"@kilocode/kilo-indexing": patch --- Fix the codebase indexing settings to show a model dropdown for the Kilo provider instead of a free-text "Embedding model" input. The Kilo embedding catalog is server-managed, so users should pick from the list rather than typing model ids by hand. While the catalog is loading the dropdown shows "Loading models…" and stays disabled instead of falling back to a placeholder text field. + +Show detailed embedding provider error responses during indexing initialization failures, so upstream gateway errors include the exact response body instead of only "Bad Request". diff --git a/packages/kilo-indexing/src/indexing/embedders/openai-compatible.ts b/packages/kilo-indexing/src/indexing/embedders/openai-compatible.ts index 5eb7500a6ae..57b770d383e 100644 --- a/packages/kilo-indexing/src/indexing/embedders/openai-compatible.ts +++ b/packages/kilo-indexing/src/indexing/embedders/openai-compatible.ts @@ -9,7 +9,7 @@ import { REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS, } from "../constants" import { getDefaultModelId, getModelQueryPrefix } from "../model-registry" -import { withValidationErrorHandling, type HttpError, formatEmbeddingError } from "../shared/validation-helpers" +import { withValidationErrorHandling, type HttpError } from "../shared/validation-helpers" import { Mutex } from "async-mutex" import { Log } from "../../util/log" @@ -28,6 +28,36 @@ interface OpenAIEmbeddingResponse { } } +type EmbeddingErrorDetail = { + status?: number + message: string + response?: unknown +} + +function json(value: unknown): string | undefined { + try { + return JSON.stringify(value) + } catch { + return undefined + } +} + +export function getEmbeddingErrorDetail(error: unknown): EmbeddingErrorDetail { + const err = error as { + status?: number + response?: { status?: number; data?: unknown; body?: unknown } + error?: unknown + message?: string + } + const response = err?.response?.data ?? err?.response?.body ?? err?.error + const msg = response ? json(response) : undefined + return { + status: err?.status ?? err?.response?.status, + message: msg ?? (error instanceof Error ? error.message : String(error)), + ...(response === undefined ? {} : { response }), + } +} + type OpenAICompatibleOptions = { headers?: Record dimensions?: number @@ -319,8 +349,14 @@ export class OpenAICompatibleEmbedder implements IEmbedder { }, } } catch (error) { + const detail = getEmbeddingErrorDetail(error) log.error("OpenAI Compatible embedder batch error", { - err: error instanceof Error ? error.message : String(error), + err: detail.message, + response: detail.response, + status: detail.status, + baseUrl: this.baseUrl, + model, + dimensions: this.dimensions, location: "OpenAICompatibleEmbedder:_embedBatchWithRetries", attempt: attempts + 1, }) @@ -345,8 +381,11 @@ export class OpenAICompatibleEmbedder implements IEmbedder { } } - // Format and throw the error - throw formatEmbeddingError(error, MAX_RETRIES) + throw new Error( + detail.status + ? `Embedding request failed after ${MAX_RETRIES} attempts with status ${detail.status}: ${detail.message}` + : `Embedding request failed after ${MAX_RETRIES} attempts: ${detail.message}`, + ) } } diff --git a/packages/kilo-indexing/test/kilocode/indexing/embedders/openai-compatible.test.ts b/packages/kilo-indexing/test/kilocode/indexing/embedders/openai-compatible.test.ts index 4bed6ea72ed..b87955c26f8 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/embedders/openai-compatible.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/embedders/openai-compatible.test.ts @@ -467,6 +467,26 @@ describe("OpenAICompatibleEmbedder", () => { ) }) + test("should surface exact OpenAI SDK response payloads for HTTP errors", async () => { + const testTexts = ["Hello world"] + const httpError = new Error("Bad Request") + ;(httpError as any).status = 400 + ;(httpError as any).response = { + status: 400, + data: { + object: "error", + message: "Provider returned error", + metadata: { + raw: '{"error":{"message":"encoding_format is not supported"}}', + }, + }, + } + + mockEmbeddingsCreate.mockRejectedValue(httpError) + + await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow("encoding_format is not supported") + }) + test("should handle errors without status codes", async () => { const testTexts = ["Hello world"] const networkError = new Error("Network timeout") diff --git a/packages/kilo-vscode/tests/unit/config-scope.test.ts b/packages/kilo-vscode/tests/unit/config-scope.test.ts index 16b433009b5..d9da45255d1 100644 --- a/packages/kilo-vscode/tests/unit/config-scope.test.ts +++ b/packages/kilo-vscode/tests/unit/config-scope.test.ts @@ -25,6 +25,18 @@ describe("splitConfigByScope", () => { expect(split.project).toEqual({}) }) + it("does not send invalid null dimensions from provider settings", () => { + const split = splitConfigByScope({ + indexing: { + provider: "kilo", + model: "mistralai/mistral-embed-2312", + }, + }) + + expect(split.global).toEqual({ indexing: { provider: "kilo", model: "mistralai/mistral-embed-2312" } }) + expect(split.project).toEqual({}) + }) + it("can write indexing enablement to global config through a global draft", () => { const split = splitConfigByScope({ username: "marius" }) const draft = { indexing: { enabled: true } } diff --git a/packages/kilo-vscode/tests/unit/config-utils.test.ts b/packages/kilo-vscode/tests/unit/config-utils.test.ts index f67105eafd7..f5842ca032b 100644 --- a/packages/kilo-vscode/tests/unit/config-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/config-utils.test.ts @@ -330,21 +330,6 @@ describe("ConfigState", () => { }) }) - describe("clearing indexing dimension for Kilo provider", () => { - it("keeps null in the draft so stale manual dimensions are deleted", () => { - const s = new ConfigState() - s.handleConfigLoaded({ indexing: { provider: "openrouter", model: "openai/text-embedding-3-small", dimension: 256 } }) - - s.updateConfig({ indexing: { provider: "kilo", model: "mistralai/codestral-embed-2505", dimension: null } }) - - expect(s.config.indexing?.dimension).toBeUndefined() - expect(s.draft.indexing?.dimension).toBeNull() - expect(JSON.parse(JSON.stringify(s.draft))).toEqual({ - indexing: { provider: "kilo", model: "mistralai/codestral-embed-2505", dimension: null }, - }) - }) - }) - describe("agent permission patches", () => { it("merges nested per-agent permission patches into existing rules", () => { const s = new ConfigState() diff --git a/packages/kilo-vscode/tests/unit/indexing-tab-kilo-no-textfield.test.ts b/packages/kilo-vscode/tests/unit/indexing-tab-kilo-no-textfield.test.ts index 2de3728475a..3dbf4a88eed 100644 --- a/packages/kilo-vscode/tests/unit/indexing-tab-kilo-no-textfield.test.ts +++ b/packages/kilo-vscode/tests/unit/indexing-tab-kilo-no-textfield.test.ts @@ -62,8 +62,9 @@ describe("IndexingTab kilo branch", () => { expect(SOURCE).toContain("disabled={kiloModels().length === 0}") }) - it("clears stale manual dimensions with null delete sentinels when using Kilo", () => { - expect(SOURCE).toContain("dimension: null") - expect(SOURCE).not.toContain("dimension: undefined })}") + it("does not send invalid null dimensions in the typed config payload", () => { + expect(SOURCE).toContain('if (next.provider === "kilo") delete next.dimension') + expect(SOURCE).not.toContain("dimension: null") + expect(SOURCE).not.toContain("dimension: undefined") }) }) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx index b7aaf13bb08..e05681ff3e8 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx @@ -92,7 +92,9 @@ const IndexingTab: Component = () => { const globalOn = createMemo(() => globalCfg().enabled === true) const updateIndexing = (partial: IndexingConfig) => { - updateConfig({ indexing: { ...cfg(), ...partial } }) + const next = { ...cfg(), ...partial } + if (next.provider === "kilo") delete next.dimension + updateConfig({ indexing: next }) } const vectorStore = () => cfg().vectorStore ?? "qdrant" @@ -117,11 +119,10 @@ const IndexingTab: Component = () => { updateIndexing({ provider: next, model, - dimension: null, }) return } - updateIndexing({ provider: next, model: undefined, dimension: null }) + updateIndexing({ provider: next, model: undefined }) } const saveEnabled = (enabled: boolean) => { @@ -130,7 +131,6 @@ const IndexingTab: Component = () => { enabled, provider: "kilo", model: knownKiloModel(cfg().model) ?? kiloDefault(), - dimension: null, }) return } @@ -144,7 +144,6 @@ const IndexingTab: Component = () => { enabled, provider: "kilo", model: knownKiloModel(cfg().model) ?? kiloDefault(), - dimension: null, }, }) return @@ -303,7 +302,7 @@ const IndexingTab: Component = () => { current={kiloModels().find((item) => item.value === knownKiloModel(cfg().model))} value={(item) => item.value} label={(item) => item.label} - onSelect={(item) => updateIndexing({ model: item?.value ?? kiloDefault(), dimension: null })} + onSelect={(item) => updateIndexing({ model: item?.value ?? kiloDefault() })} variant="secondary" size="small" triggerVariant="settings" diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts index 9e7c4ee2852..ca0c91c3d39 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts @@ -67,7 +67,7 @@ export interface IndexingConfig { enabled?: boolean provider?: IndexingProvider model?: string - dimension?: number | null + dimension?: number vectorStore?: "lancedb" | "qdrant" kilo?: { apiKey?: string; baseUrl?: string; organizationId?: string } openai?: { apiKey?: string } diff --git a/packages/kilo-vscode/webview-ui/src/utils/config-scope.ts b/packages/kilo-vscode/webview-ui/src/utils/config-scope.ts index d55092b85aa..1df89a765ee 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/config-scope.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/config-scope.ts @@ -4,7 +4,7 @@ import type { Config } from "../types/messages" // global one. Settings that are inherently per-repository (e.g. commit message // conventions) belong here so they don't leak across workspaces. const PROJECT_SCOPED_KEYS: ReadonlySet = new Set(["commit_message"]) -const PROJECT_INDEXING_KEYS: ReadonlySet = new Set(["enabled"]) +const PROJECT_INDEXING_KEYS: ReadonlySet = new Set(["enabled", "dimension"]) function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value) @@ -12,8 +12,9 @@ function isRecord(value: unknown): value is Record { function splitIndexing(value: unknown) { if (!isRecord(value)) return { global: value, project: undefined } - const global = Object.fromEntries(Object.entries(value).filter(([key]) => !PROJECT_INDEXING_KEYS.has(key))) - const project = Object.fromEntries(Object.entries(value).filter(([key]) => PROJECT_INDEXING_KEYS.has(key))) + const entries = Object.entries(value).map(([key, item]) => [key, key === "dimension" ? null : item] as const) + const global = Object.fromEntries(entries.filter(([key]) => !PROJECT_INDEXING_KEYS.has(key))) + const project = Object.fromEntries(entries.filter(([key]) => PROJECT_INDEXING_KEYS.has(key))) return { global: Object.keys(global).length > 0 ? global : undefined, project: Object.keys(project).length > 0 ? project : undefined, From 3a34af603cc0acbe2615373ed9db685ba03e3dd9 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 13 May 2026 10:06:59 +0200 Subject: [PATCH 07/11] fix(indexing): surface Qdrant dimension errors --- .../indexing/vector-store/qdrant-client.ts | 45 ++++++++++++++- .../vector-store/qdrant-client.test.ts | 56 ++++++++++++++++++- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/packages/kilo-indexing/src/indexing/vector-store/qdrant-client.ts b/packages/kilo-indexing/src/indexing/vector-store/qdrant-client.ts index 44b45240966..2459771367d 100644 --- a/packages/kilo-indexing/src/indexing/vector-store/qdrant-client.ts +++ b/packages/kilo-indexing/src/indexing/vector-store/qdrant-client.ts @@ -18,6 +18,31 @@ const KEY = { const METADATA_ID = "f946a536-9af4-4f1f-9f95-7d6efb4647d5" +function json(value: unknown): string | undefined { + try { + return JSON.stringify(value) + } catch { + return undefined + } +} + +function qdrantErrorDetail(error: unknown) { + const err = error as { + status?: number + statusCode?: number + response?: { status?: number; data?: unknown; body?: unknown } + data?: unknown + body?: unknown + message?: string + } + const response = err?.response?.data ?? err?.response?.body ?? err?.data ?? err?.body + return { + status: err?.status ?? err?.statusCode ?? err?.response?.status, + message: json(response) ?? (error instanceof Error ? error.message : String(error)), + response, + } +} + /** * Qdrant implementation of the vector store interface */ @@ -416,6 +441,13 @@ export class QdrantVectorStore implements IVectorStore { }>, ): Promise { try { + const mismatch = points.find((point) => point.vector.length !== this.vectorSize) + if (mismatch) { + throw new Error( + `Qdrant vector dimension mismatch before upsert: expected ${this.vectorSize}, got ${mismatch.vector.length}`, + ) + } + const processedPoints = points.map((point) => { if (point.payload?.filePath) { const segments = point.payload.filePath.split(path.sep).filter(Boolean) @@ -439,8 +471,17 @@ export class QdrantVectorStore implements IVectorStore { wait: true, }) } catch (error) { - log.error("Failed to upsert points", { error }) - throw error + const detail = qdrantErrorDetail(error) + log.error("Failed to upsert points", { + error: detail.message, + response: detail.response, + status: detail.status, + collection: this.collectionName, + expectedVectorSize: this.vectorSize, + firstVectorSize: points[0]?.vector.length, + pointCount: points.length, + }) + throw new Error(`Qdrant upsert failed: ${detail.message}`) } } diff --git a/packages/kilo-indexing/test/kilocode/indexing/vector-store/qdrant-client.test.ts b/packages/kilo-indexing/test/kilocode/indexing/vector-store/qdrant-client.test.ts index 8a580bcde83..3dc350fb3c9 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/vector-store/qdrant-client.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/vector-store/qdrant-client.test.ts @@ -1092,6 +1092,10 @@ describe("QdrantVectorStore", () => { }) describe("upsertPoints", () => { + beforeEach(() => { + vectorStore = new QdrantVectorStore(mockWorkspacePath, mockQdrantUrl, 3, mockApiKey) + }) + test("should correctly call qdrantClient.upsert with processed points", async () => { const mockPoints = [ { @@ -1261,10 +1265,60 @@ describe("QdrantVectorStore", () => { const upsertError = new Error("Upsert failed") mockUpsert.mockRejectedValue(upsertError) - await expect(vectorStore.upsertPoints(mockPoints)).rejects.toThrow(upsertError) + await expect(vectorStore.upsertPoints(mockPoints)).rejects.toThrow("Qdrant upsert failed: Upsert failed") expect(mockUpsert).toHaveBeenCalledTimes(1) }) + + test("should fail before upsert when vectors do not match the configured dimension", async () => { + const mockPoints = [ + { + id: "test-id-1", + vector: [0.1, 0.2, 0.3, 0.4], + payload: { + filePath: "src/test.ts", + content: "test content", + startLine: 1, + endLine: 1, + }, + }, + ] + + await expect(vectorStore.upsertPoints(mockPoints)).rejects.toThrow( + "Qdrant vector dimension mismatch before upsert: expected 3, got 4", + ) + + expect(mockUpsert).not.toHaveBeenCalled() + }) + + test("should include Qdrant response details when upsert fails", async () => { + const mockPoints = [ + { + id: "test-id-1", + vector: [0.1, 0.2, 0.3], + payload: { + filePath: "src/test.ts", + content: "test content", + startLine: 1, + endLine: 1, + }, + }, + ] + const upsertError = new Error("Bad Request") as Error & { + status?: number + response?: { status: number; data: unknown } + } + upsertError.status = 400 + upsertError.response = { + status: 400, + data: { status: { error: "Wrong input: Vector dimension error: expected dim: 256, got 1536" } }, + } + mockUpsert.mockRejectedValue(upsertError) + + await expect(vectorStore.upsertPoints(mockPoints)).rejects.toThrow( + "Wrong input: Vector dimension error: expected dim: 256, got 1536", + ) + }) }) describe("search", () => { From 2db363bf1fb0403635cf3d39648c1ba4388b8f82 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 13 May 2026 10:08:41 +0200 Subject: [PATCH 08/11] docs(changeset): mention Qdrant indexing errors --- .changeset/indexing-kilo-model-select.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/indexing-kilo-model-select.md b/.changeset/indexing-kilo-model-select.md index 9dd27683056..3d29381864c 100644 --- a/.changeset/indexing-kilo-model-select.md +++ b/.changeset/indexing-kilo-model-select.md @@ -5,4 +5,4 @@ Fix the codebase indexing settings to show a model dropdown for the Kilo provider instead of a free-text "Embedding model" input. The Kilo embedding catalog is server-managed, so users should pick from the list rather than typing model ids by hand. While the catalog is loading the dropdown shows "Loading models…" and stays disabled instead of falling back to a placeholder text field. -Show detailed embedding provider error responses during indexing initialization failures, so upstream gateway errors include the exact response body instead of only "Bad Request". +Show detailed embedding provider and Qdrant vector-store errors during indexing initialization failures, so failures include the exact response or dimension mismatch instead of only "Bad Request". From c883b76836b46ab1714a0141cef91400a22aaa7d Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 13 May 2026 10:27:04 +0200 Subject: [PATCH 09/11] fix(indexing): surface Qdrant search dimension errors --- .../indexing/vector-store/qdrant-client.ts | 19 +++++++++-- .../vector-store/qdrant-client.test.ts | 34 ++++++++++++++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/kilo-indexing/src/indexing/vector-store/qdrant-client.ts b/packages/kilo-indexing/src/indexing/vector-store/qdrant-client.ts index 2459771367d..749d0dad0c6 100644 --- a/packages/kilo-indexing/src/indexing/vector-store/qdrant-client.ts +++ b/packages/kilo-indexing/src/indexing/vector-store/qdrant-client.ts @@ -514,6 +514,12 @@ export class QdrantVectorStore implements IVectorStore { maxResults?: number, ): Promise { try { + if (queryVector.length !== this.vectorSize) { + throw new Error( + `Qdrant query vector dimension mismatch before search: expected ${this.vectorSize}, got ${queryVector.length}`, + ) + } + let filter: | { must: Array<{ key: string; match: { value: string } }> @@ -573,8 +579,17 @@ export class QdrantVectorStore implements IVectorStore { return filteredPoints as VectorStoreSearchResult[] } catch (error) { - log.error("Failed to search points", { error }) - throw error + const detail = qdrantErrorDetail(error) + log.error("Failed to search points", { + error: detail.message, + response: detail.response, + status: detail.status, + collection: this.collectionName, + expectedVectorSize: this.vectorSize, + queryVectorSize: queryVector.length, + directoryPrefix, + }) + throw new Error(`Qdrant search failed: ${detail.message}`) } } diff --git a/packages/kilo-indexing/test/kilocode/indexing/vector-store/qdrant-client.test.ts b/packages/kilo-indexing/test/kilocode/indexing/vector-store/qdrant-client.test.ts index 3dc350fb3c9..b6b77f71f6c 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/vector-store/qdrant-client.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/vector-store/qdrant-client.test.ts @@ -1322,6 +1322,10 @@ describe("QdrantVectorStore", () => { }) describe("search", () => { + beforeEach(() => { + vectorStore = new QdrantVectorStore(mockWorkspacePath, mockQdrantUrl, 3, mockApiKey) + }) + test("should correctly call qdrantClient.query and transform results", async () => { const queryVector = [0.1, 0.2, 0.3] const mockQdrantResults = { @@ -1611,11 +1615,39 @@ describe("QdrantVectorStore", () => { const queryError = new Error("Query failed") mockQuery.mockRejectedValue(queryError) - await expect(vectorStore.search(queryVector)).rejects.toThrow(queryError) + await expect(vectorStore.search(queryVector)).rejects.toThrow("Qdrant search failed: Query failed") expect(mockQuery).toHaveBeenCalledTimes(1) }) + test("should fail before search when query vector does not match the configured dimension", async () => { + const queryVector = [0.1, 0.2, 0.3, 0.4] + + await expect(vectorStore.search(queryVector)).rejects.toThrow( + "Qdrant query vector dimension mismatch before search: expected 3, got 4", + ) + + expect(mockQuery).not.toHaveBeenCalled() + }) + + test("should include Qdrant response details when query fails", async () => { + const queryVector = [0.1, 0.2, 0.3] + const queryError = new Error("Bad Request") as Error & { + status?: number + response?: { status: number; data: unknown } + } + queryError.status = 400 + queryError.response = { + status: 400, + data: { status: { error: "Wrong input: Vector dimension error: expected dim: 256, got 1536" } }, + } + mockQuery.mockRejectedValue(queryError) + + await expect(vectorStore.search(queryVector)).rejects.toThrow( + "Wrong input: Vector dimension error: expected dim: 256, got 1536", + ) + }) + test("should use constants DEFAULT_MAX_SEARCH_RESULTS and DEFAULT_SEARCH_MIN_SCORE correctly", async () => { const queryVector = [0.1, 0.2, 0.3] const mockQdrantResults = { points: [] } From 3a7dd20e09a24dd0e313762bf8cb519deb2bf82b Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 14 May 2026 15:51:22 +0200 Subject: [PATCH 10/11] fix(vscode): keep dimension scoping unchanged --- packages/kilo-vscode/tests/unit/config-scope.test.ts | 12 ------------ .../kilo-vscode/webview-ui/src/utils/config-scope.ts | 7 +++---- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/config-scope.test.ts b/packages/kilo-vscode/tests/unit/config-scope.test.ts index d9da45255d1..16b433009b5 100644 --- a/packages/kilo-vscode/tests/unit/config-scope.test.ts +++ b/packages/kilo-vscode/tests/unit/config-scope.test.ts @@ -25,18 +25,6 @@ describe("splitConfigByScope", () => { expect(split.project).toEqual({}) }) - it("does not send invalid null dimensions from provider settings", () => { - const split = splitConfigByScope({ - indexing: { - provider: "kilo", - model: "mistralai/mistral-embed-2312", - }, - }) - - expect(split.global).toEqual({ indexing: { provider: "kilo", model: "mistralai/mistral-embed-2312" } }) - expect(split.project).toEqual({}) - }) - it("can write indexing enablement to global config through a global draft", () => { const split = splitConfigByScope({ username: "marius" }) const draft = { indexing: { enabled: true } } diff --git a/packages/kilo-vscode/webview-ui/src/utils/config-scope.ts b/packages/kilo-vscode/webview-ui/src/utils/config-scope.ts index 1df89a765ee..d55092b85aa 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/config-scope.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/config-scope.ts @@ -4,7 +4,7 @@ import type { Config } from "../types/messages" // global one. Settings that are inherently per-repository (e.g. commit message // conventions) belong here so they don't leak across workspaces. const PROJECT_SCOPED_KEYS: ReadonlySet = new Set(["commit_message"]) -const PROJECT_INDEXING_KEYS: ReadonlySet = new Set(["enabled", "dimension"]) +const PROJECT_INDEXING_KEYS: ReadonlySet = new Set(["enabled"]) function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value) @@ -12,9 +12,8 @@ function isRecord(value: unknown): value is Record { function splitIndexing(value: unknown) { if (!isRecord(value)) return { global: value, project: undefined } - const entries = Object.entries(value).map(([key, item]) => [key, key === "dimension" ? null : item] as const) - const global = Object.fromEntries(entries.filter(([key]) => !PROJECT_INDEXING_KEYS.has(key))) - const project = Object.fromEntries(entries.filter(([key]) => PROJECT_INDEXING_KEYS.has(key))) + const global = Object.fromEntries(Object.entries(value).filter(([key]) => !PROJECT_INDEXING_KEYS.has(key))) + const project = Object.fromEntries(Object.entries(value).filter(([key]) => PROJECT_INDEXING_KEYS.has(key))) return { global: Object.keys(global).length > 0 ? global : undefined, project: Object.keys(project).length > 0 ? project : undefined, From d53e31d24eac4bbdfc945030b9e851eb49032ba6 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 14 May 2026 15:53:56 +0200 Subject: [PATCH 11/11] style(vscode): avoid em dash in indexing comment --- .../webview-ui/src/components/settings/IndexingTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx index e05681ff3e8..fe2825775de 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx @@ -290,7 +290,7 @@ const IndexingTab: Component = () => { } > {/* Kilo provider: model is chosen from the Cloud-managed catalog only. - No free-text override or dimension field — the catalog entry + No free-text override or dimension field, the catalog entry determines the dimension server-side. */}