mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
fix: code indexing on kilo core
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Stabilize code indexing workers, retry Kilo model catalog downloads, reduce progress log noise, and show indexing failures as TUI notifications instead of writing over the terminal interface.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Support configuring code indexing separately for global and project settings in Kilo Console, the CLI TUI, and VS Code.
|
||||
@@ -23,11 +23,13 @@ test("config writes include the selected directory", async () => {
|
||||
|
||||
await client.saveConfig(query, { permission: { edit: { "*": "allow" } } })
|
||||
await client.unsetConfig(query, [["permission", "edit"]])
|
||||
await client.patchConfig(query, { indexing: { provider: "ollama" } }, [["indexing", "model"]])
|
||||
|
||||
expect(calls).toHaveLength(2)
|
||||
expect(calls).toHaveLength(3)
|
||||
|
||||
const save = calls[0]
|
||||
const unset = calls[1]
|
||||
const patch = calls[2]
|
||||
expect(save.method).toBe("PATCH")
|
||||
expect(new URL(save.url).searchParams.get("directory")).toBe("/tmp/project")
|
||||
expect(save.body).toEqual({ scope: "project", set: { permission: { edit: { "*": "allow" } } } })
|
||||
@@ -35,4 +37,12 @@ test("config writes include the selected directory", async () => {
|
||||
expect(unset.method).toBe("PATCH")
|
||||
expect(new URL(unset.url).searchParams.get("directory")).toBe("/tmp/project")
|
||||
expect(unset.body).toEqual({ scope: "project", unset: [["permission", "edit"]] })
|
||||
|
||||
expect(patch.method).toBe("PATCH")
|
||||
expect(new URL(patch.url).searchParams.get("directory")).toBe("/tmp/project")
|
||||
expect(patch.body).toEqual({
|
||||
scope: "project",
|
||||
set: { indexing: { provider: "ollama" } },
|
||||
unset: [["indexing", "model"]],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
FormatterStatusResponse,
|
||||
GlobalHealthResponse,
|
||||
GlobalEvent,
|
||||
KiloEmbeddingModelCatalog,
|
||||
LspStatusResponse,
|
||||
McpStatusResponse,
|
||||
Pty as PtyInfo,
|
||||
@@ -400,6 +401,10 @@ export async function load(input: Query): Promise<Snapshot> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadEmbeddingModels(input: Query): Promise<KiloEmbeddingModelCatalog> {
|
||||
return demand("Kilo embedding models", await client(input).indexing.models())
|
||||
}
|
||||
|
||||
export async function loadProjects(input: ProjectQuery): Promise<ProjectItem[]> {
|
||||
const sdk = client(input)
|
||||
const dir = value(input.dir)
|
||||
@@ -611,16 +616,24 @@ export function ptyWsUrl(input: Query, pty: string, cursor = 0) {
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
export async function saveConfig(input: Query, patch: Partial<ConfigPatch>) {
|
||||
export async function patchConfig(input: Query, patch: Partial<ConfigPatch>, unset?: ConfigUnset) {
|
||||
const sdk = client(input)
|
||||
const result = await sdk.config.overlayUpdate({ directory: value(input.dir), scope: input.scope, set: patch })
|
||||
const set = Object.keys(patch).length ? patch : undefined
|
||||
const result = await sdk.config.overlayUpdate({
|
||||
directory: value(input.dir),
|
||||
scope: input.scope,
|
||||
set,
|
||||
unset,
|
||||
})
|
||||
return demand("Update config", result)
|
||||
}
|
||||
|
||||
export async function saveConfig(input: Query, patch: Partial<ConfigPatch>) {
|
||||
return patchConfig(input, patch)
|
||||
}
|
||||
|
||||
export async function unsetConfig(input: Query, unset: ConfigUnset) {
|
||||
const sdk = client(input)
|
||||
const result = await sdk.config.overlayUpdate({ directory: value(input.dir), scope: input.scope, unset })
|
||||
return demand("Update config", result)
|
||||
return patchConfig(input, {}, unset)
|
||||
}
|
||||
|
||||
export async function saveModelState(input: Query, favorite: ModelRef[]) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
load,
|
||||
loadCached,
|
||||
loadProjects,
|
||||
patchConfig,
|
||||
resolveServer,
|
||||
saveCached,
|
||||
saveConfig,
|
||||
@@ -133,6 +134,10 @@ export function ConfigProvider(props: { children?: JSX.Element }) {
|
||||
run("Saving config", () => saveConfig(target(), patch))
|
||||
}
|
||||
|
||||
function patch(update: Partial<ConfigPatch>, unset?: ConfigUnset) {
|
||||
run("Saving config", () => patchConfig(target(), update, unset))
|
||||
}
|
||||
|
||||
function unset(paths: ConfigUnset) {
|
||||
run("Saving config", () => unsetConfig(target(), paths))
|
||||
}
|
||||
@@ -150,6 +155,7 @@ export function ConfigProvider(props: { children?: JSX.Element }) {
|
||||
fail,
|
||||
run,
|
||||
save,
|
||||
patch,
|
||||
unset,
|
||||
tui,
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export type Ctx = {
|
||||
fail: (message: string) => void
|
||||
run: (label: string, job: () => Promise<unknown>, task?: Task) => void
|
||||
save: (patch: Partial<ConfigPatch>) => void
|
||||
patch: (patch: Partial<ConfigPatch>, unset?: ConfigUnset) => void
|
||||
unset: (paths: ConfigUnset) => void
|
||||
tui: (patch: TuiPatch) => void
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@ import { Button } from "@kilocode/kilo-web-ui/button"
|
||||
import { Card } from "@kilocode/kilo-web-ui/card"
|
||||
import { ConfigRow, SectionTitle } from "@kilocode/kilo-web-ui/console"
|
||||
import { IconButton } from "@kilocode/kilo-web-ui/icon-button"
|
||||
import { CountTag, Tag } from "@kilocode/kilo-web-ui/tag"
|
||||
import { CustomSelect, type SelectOption } from "../../components/CustomSelect"
|
||||
import { SearchField } from "../../components/SearchField"
|
||||
import { useConfig } from "../../context/config"
|
||||
import { settings } from "../../shared/navigation"
|
||||
import { toolCapabilities, toolName } from "../../shared/utils"
|
||||
import { ConfigPage, SourceBadge } from "./ConfigPage"
|
||||
import { ConfigCountTag as CountTag, ConfigPage, ConfigTag as Tag, SourceBadge } from "./ConfigPage"
|
||||
import { ActionSelect, label as actionLabel, tone as actionTone } from "./PermissionsRoute"
|
||||
import { agentEditable, agentTitle, snippets, useAgentBuilder, type AgentEntry, type AgentItem } from "./state/agents"
|
||||
import type { PermissionAction } from "./state/permissions"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Button } from "@kilocode/kilo-web-ui/button"
|
||||
import { Card } from "@kilocode/kilo-web-ui/card"
|
||||
import { Tag } from "@kilocode/kilo-web-ui/tag"
|
||||
import { ConfigPage } from "./ConfigPage"
|
||||
import { ConfigPage, ConfigTag as Tag } from "./ConfigPage"
|
||||
import { useTuiNotificationSettings } from "./state/ui"
|
||||
|
||||
function Toggle(props: {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { For, Show } from "solid-js"
|
||||
import { Button } from "@kilocode/kilo-web-ui/button"
|
||||
import { Card } from "@kilocode/kilo-web-ui/card"
|
||||
import { Tag } from "@kilocode/kilo-web-ui/tag"
|
||||
import { CustomSelect, type SelectOption } from "../../components/CustomSelect"
|
||||
import { SearchField } from "../../components/SearchField"
|
||||
import { ConfigPage } from "./ConfigPage"
|
||||
import { ConfigPage, ConfigTag as Tag } from "./ConfigPage"
|
||||
import { type Theme, themeTitle, useTuiUiSettings } from "./state/ui"
|
||||
|
||||
const diffs = [
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { JSX } from "solid-js"
|
||||
import { Show } from "solid-js"
|
||||
import { SourceBadge as UiSourceBadge } from "@kilocode/kilo-web-ui/console"
|
||||
import { ConfigCountTag, ConfigTag, SourceBadge as UiSourceBadge } from "@kilocode/kilo-web-ui/console"
|
||||
|
||||
export { ConfigCountTag, ConfigTag }
|
||||
|
||||
export function ConfigPage(props: {
|
||||
title: JSX.Element
|
||||
|
||||
@@ -2,8 +2,7 @@ import { For, Show } from "solid-js"
|
||||
import { Button } from "@kilocode/kilo-web-ui/button"
|
||||
import { ConfigRow, SectionTitle } from "@kilocode/kilo-web-ui/console"
|
||||
import { IconButton } from "@kilocode/kilo-web-ui/icon-button"
|
||||
import { CountTag, Tag } from "@kilocode/kilo-web-ui/tag"
|
||||
import { ConfigPage, SourceBadge } from "./ConfigPage"
|
||||
import { ConfigCountTag as CountTag, ConfigPage, ConfigTag as Tag, SourceBadge } from "./ConfigPage"
|
||||
import { useFormatterSettings, type ToolRow } from "./state/formatters"
|
||||
|
||||
type Kind = "formatter" | "lsp"
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
import { Button } from "@kilocode/kilo-web-ui/button"
|
||||
import { Card } from "@kilocode/kilo-web-ui/card"
|
||||
import type { IndexingConfig } from "@kilocode/sdk/v2/client"
|
||||
import { For, Show, createEffect, createMemo, createResource, createSignal, type JSX } from "solid-js"
|
||||
import { CustomSelect, type SelectOption } from "../../components/CustomSelect"
|
||||
import { loadEmbeddingModels } from "../../client"
|
||||
import { useConfig } from "../../context/config"
|
||||
import { ConfigPage, ConfigTag as Tag, SourceBadge } from "./ConfigPage"
|
||||
import { clean, clone, merge, providerPatch, removed, shouldSync, validate } from "./state/indexing"
|
||||
|
||||
type Provider = NonNullable<IndexingConfig["provider"]>
|
||||
type ProviderValue = Provider | ""
|
||||
type Store = NonNullable<IndexingConfig["vectorStore"]>
|
||||
type Field = { key: string; label: string; placeholder: string; secret?: boolean }
|
||||
|
||||
const providers = [
|
||||
{ value: "", label: "Automatic" },
|
||||
{ value: "kilo", label: "Kilo" },
|
||||
{ value: "openai", label: "OpenAI" },
|
||||
{ value: "ollama", label: "Ollama (local)" },
|
||||
{ value: "openai-compatible", label: "OpenAI-compatible" },
|
||||
{ value: "gemini", label: "Gemini" },
|
||||
{ value: "mistral", label: "Mistral" },
|
||||
{ value: "vercel-ai-gateway", label: "Vercel AI Gateway" },
|
||||
{ value: "bedrock", label: "AWS Bedrock" },
|
||||
{ value: "openrouter", label: "OpenRouter" },
|
||||
{ value: "voyage", label: "Voyage" },
|
||||
] satisfies SelectOption<ProviderValue>[]
|
||||
|
||||
const stores = [
|
||||
{ value: "lancedb", label: "LanceDB (default)" },
|
||||
{ value: "qdrant", label: "Qdrant" },
|
||||
] satisfies SelectOption<Store>[]
|
||||
|
||||
const fields: Record<Provider, Field[]> = {
|
||||
kilo: [],
|
||||
openai: [{ key: "apiKey", label: "API key", placeholder: "sk-...", secret: true }],
|
||||
ollama: [{ key: "baseUrl", label: "Base URL", placeholder: "http://localhost:11434" }],
|
||||
"openai-compatible": [
|
||||
{ key: "baseUrl", label: "Base URL", placeholder: "https://api.example.com/v1" },
|
||||
{ key: "apiKey", label: "API key", placeholder: "sk-...", secret: true },
|
||||
],
|
||||
gemini: [{ key: "apiKey", label: "API key", placeholder: "AI...", secret: true }],
|
||||
mistral: [{ key: "apiKey", label: "API key", placeholder: "...", secret: true }],
|
||||
"vercel-ai-gateway": [{ key: "apiKey", label: "API key", placeholder: "...", secret: true }],
|
||||
bedrock: [
|
||||
{ key: "region", label: "AWS region", placeholder: "us-east-1" },
|
||||
{ key: "profile", label: "AWS profile", placeholder: "default" },
|
||||
],
|
||||
openrouter: [
|
||||
{ key: "apiKey", label: "API key", placeholder: "sk-or-...", secret: true },
|
||||
{ key: "specificProvider", label: "Specific provider", placeholder: "Optional routing provider" },
|
||||
],
|
||||
voyage: [{ key: "apiKey", label: "API key", placeholder: "pa-...", secret: true }],
|
||||
}
|
||||
|
||||
function options(input: IndexingConfig, provider: Provider) {
|
||||
const value = input[provider]
|
||||
if (!value || typeof value !== "object") return {}
|
||||
return value as Record<string, string | undefined>
|
||||
}
|
||||
|
||||
function FieldCard(props: { label: string; description?: string; actions?: JSX.Element; children: JSX.Element }) {
|
||||
return (
|
||||
<div class="ui-field agent-builder-field">
|
||||
<div class="agent-builder-field-head">
|
||||
<div>
|
||||
<span>{props.label}</span>
|
||||
<Show when={props.description}>{(description) => <small>{description()}</small>}</Show>
|
||||
</div>
|
||||
<Show when={props.actions}>{(actions) => <div class="agent-builder-field-actions">{actions()}</div>}</Show>
|
||||
</div>
|
||||
<div class="agent-builder-control">{props.children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Toggle(props: {
|
||||
label: string
|
||||
description: string
|
||||
checked: boolean
|
||||
disabled?: boolean
|
||||
source?: string
|
||||
inherited?: boolean
|
||||
overridden?: boolean
|
||||
onChange: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
class="ui-toggle"
|
||||
classList={{ selected: props.checked }}
|
||||
type="button"
|
||||
aria-pressed={props.checked}
|
||||
disabled={props.disabled}
|
||||
onClick={props.onChange}
|
||||
>
|
||||
<span>
|
||||
<strong>{props.label}</strong>
|
||||
<small>{props.description}</small>
|
||||
</span>
|
||||
<span class="indexing-toggle-tags">
|
||||
<SourceBadge source={props.source} inherited={props.inherited} overridden={props.overridden} />
|
||||
<Tag tone={props.checked ? "success" : "neutral"}>{props.checked ? "On" : "Off"}</Tag>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function IndexingRoute() {
|
||||
const ctx = useConfig()
|
||||
const [draft, setDraft] = createSignal<IndexingConfig>({})
|
||||
const [source, setSource] = createSignal("")
|
||||
const [dirty, setDirty] = createSignal(false)
|
||||
const scope = () => ctx.query()?.scope ?? "global"
|
||||
const [selected, setSelected] = createSignal(scope())
|
||||
const project = () => scope() === "project"
|
||||
const global = createMemo(() => ctx.data()?.overlay.global.indexing ?? {})
|
||||
const local = createMemo(() => {
|
||||
const overlay = ctx.data()?.overlay
|
||||
if (!overlay) return {}
|
||||
return (project() ? overlay.project.indexing : overlay.global.indexing) ?? {}
|
||||
})
|
||||
const view = createMemo(() => (project() ? merge(global(), draft()) : draft()))
|
||||
const provider = createMemo(() => view().provider)
|
||||
const store = createMemo<Store>(() => view().vectorStore ?? "lancedb")
|
||||
const [catalog] = createResource(ctx.query, loadEmbeddingModels)
|
||||
const kiloModels = createMemo<SelectOption<string>[]>(() => {
|
||||
const models = catalog()?.models ?? []
|
||||
if (models.length === 0) return [{ value: "", label: "No Kilo embedding models available", disabled: true }]
|
||||
return models.map((model) => ({
|
||||
value: model.id,
|
||||
label: `${model.name} (${model.note ? `${model.note}, ` : ""}${model.dimension}d)`,
|
||||
}))
|
||||
})
|
||||
const kiloModel = createMemo(() => {
|
||||
const data = catalog()
|
||||
if (!data) return ""
|
||||
const model = view().model ?? data.defaultModel
|
||||
return data.aliases[model] ?? model
|
||||
})
|
||||
const errors = createMemo(() => validate(clean(draft())))
|
||||
const overridden = createMemo(() => Object.keys(local()).length > 0)
|
||||
|
||||
createEffect(() => {
|
||||
const current = scope()
|
||||
const next = local()
|
||||
const key = JSON.stringify(next)
|
||||
if (!shouldSync(selected(), current, dirty(), source(), key)) return
|
||||
setSelected(current)
|
||||
setSource(key)
|
||||
setDraft(clone(next))
|
||||
setDirty(false)
|
||||
})
|
||||
|
||||
function field(path: string) {
|
||||
return ctx.data()?.overlay.fields[`indexing.${path}`]
|
||||
}
|
||||
|
||||
function update(patch: IndexingConfig) {
|
||||
setDraft((current) => merge(current, patch))
|
||||
setDirty(true)
|
||||
}
|
||||
|
||||
function text(key: keyof IndexingConfig, value: string) {
|
||||
update({ [key]: value || undefined })
|
||||
}
|
||||
|
||||
function number(key: keyof IndexingConfig, value: string) {
|
||||
update({ [key]: value ? Number(value) : undefined })
|
||||
}
|
||||
|
||||
function providerField(group: Provider, key: string, value: string) {
|
||||
update({ [group]: { ...options(draft(), group), [key]: value || undefined } })
|
||||
}
|
||||
|
||||
function selectProvider(value: ProviderValue) {
|
||||
update(providerPatch(value, catalog()?.defaultModel))
|
||||
}
|
||||
|
||||
function save() {
|
||||
const next = clean(draft())
|
||||
const unset = removed(local(), next)
|
||||
ctx.patch({ indexing: next }, unset.length ? unset : undefined)
|
||||
setDraft(next)
|
||||
setSource(JSON.stringify(next))
|
||||
setDirty(false)
|
||||
}
|
||||
|
||||
function reset() {
|
||||
ctx.unset([["indexing"]])
|
||||
setDraft({})
|
||||
setSource("{}")
|
||||
setDirty(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigPage
|
||||
title="Code Indexing"
|
||||
description={
|
||||
project()
|
||||
? "Configure semantic code search for this project. Unchanged values inherit from global settings."
|
||||
: "Configure semantic code search defaults for every project."
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<Show when={overridden()}>
|
||||
<Button variant="secondary" disabled={Boolean(ctx.saving())} onClick={reset}>
|
||||
{project() ? "Use global settings" : "Clear settings"}
|
||||
</Button>
|
||||
</Show>
|
||||
<Button variant="primary" disabled={Boolean(ctx.saving()) || !dirty() || errors().length > 0} onClick={save}>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div class="builder indexing-builder">
|
||||
<section class="builder-form agent-builder-stack">
|
||||
<Card class="ui-card agent-builder-card" padding={0}>
|
||||
<header class="ui-card-header">
|
||||
<div>
|
||||
<h2>Indexing</h2>
|
||||
<p>Build and maintain an embedding index used by semantic search.</p>
|
||||
</div>
|
||||
<Tag>{project() ? "Project" : "Global"}</Tag>
|
||||
</header>
|
||||
<div class="ui-form agent-builder-form">
|
||||
<Toggle
|
||||
label="Enable indexing"
|
||||
description={
|
||||
project() && draft().enabled === undefined
|
||||
? `Inherited from global settings (${global().enabled ? "on" : "off"}).`
|
||||
: "Scan source files and keep their semantic index up to date."
|
||||
}
|
||||
checked={view().enabled ?? false}
|
||||
disabled={Boolean(ctx.saving())}
|
||||
source={field("enabled")?.source}
|
||||
inherited={field("enabled")?.inherited}
|
||||
overridden={field("enabled")?.overridden}
|
||||
onChange={() => update({ enabled: !(view().enabled ?? false) })}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="ui-card agent-builder-card" padding={0}>
|
||||
<header class="ui-card-header">
|
||||
<div>
|
||||
<h2>Embeddings</h2>
|
||||
<p>Select the provider and model used to turn code into searchable vectors.</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="ui-form agent-builder-form">
|
||||
<FieldCard
|
||||
label="Provider"
|
||||
description="Automatic uses Kilo when signed in, otherwise the provider runtime default."
|
||||
actions={
|
||||
<SourceBadge
|
||||
source={field("provider")?.source}
|
||||
inherited={field("provider")?.inherited}
|
||||
overridden={field("provider")?.overridden}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CustomSelect
|
||||
class="indexing-select"
|
||||
label="Embedding provider"
|
||||
value={provider() ?? ""}
|
||||
options={providers}
|
||||
disabled={Boolean(ctx.saving())}
|
||||
onSelect={selectProvider}
|
||||
/>
|
||||
</FieldCard>
|
||||
|
||||
<FieldCard
|
||||
label="Model"
|
||||
description={
|
||||
provider() === "kilo"
|
||||
? "Select a Kilo-hosted embedding model."
|
||||
: "Leave empty to use the provider's default embedding model."
|
||||
}
|
||||
actions={
|
||||
<SourceBadge
|
||||
source={field("model")?.source}
|
||||
inherited={field("model")?.inherited}
|
||||
overridden={field("model")?.overridden}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={provider() === "kilo"}
|
||||
fallback={
|
||||
<input
|
||||
value={view().model ?? ""}
|
||||
placeholder="Provider default"
|
||||
disabled={Boolean(ctx.saving())}
|
||||
onInput={(event) => text("model", event.currentTarget.value)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CustomSelect
|
||||
class="indexing-select"
|
||||
label="Kilo embedding model"
|
||||
value={kiloModel()}
|
||||
options={kiloModels()}
|
||||
disabled={Boolean(ctx.saving()) || !catalog()?.models.length}
|
||||
onSelect={(value) => update({ model: value, dimension: undefined })}
|
||||
/>
|
||||
</Show>
|
||||
</FieldCard>
|
||||
|
||||
<FieldCard
|
||||
label="Vector dimension"
|
||||
description="Leave empty to derive the dimension from known model metadata."
|
||||
actions={
|
||||
<SourceBadge
|
||||
source={field("dimension")?.source}
|
||||
inherited={field("dimension")?.inherited}
|
||||
overridden={field("dimension")?.overridden}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={view().dimension ?? ""}
|
||||
placeholder="Auto-detect"
|
||||
disabled={Boolean(ctx.saving()) || provider() === "kilo"}
|
||||
onInput={(event) => number("dimension", event.currentTarget.value)}
|
||||
/>
|
||||
</FieldCard>
|
||||
|
||||
<Show when={provider() === "kilo"}>
|
||||
<div class="indexing-note">
|
||||
Kilo embeddings use the account currently signed in to this Kilo server. Model dimensions are supplied
|
||||
by the catalog.
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={provider()} keyed>
|
||||
{(group) => (
|
||||
<For each={fields[group]}>
|
||||
{(item) => {
|
||||
const meta = () => field(`${group}.${item.key}`)
|
||||
return (
|
||||
<FieldCard
|
||||
label={item.label}
|
||||
description="Provider-specific connection setting."
|
||||
actions={
|
||||
<SourceBadge
|
||||
source={meta()?.source}
|
||||
inherited={meta()?.inherited}
|
||||
overridden={meta()?.overridden}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<input
|
||||
type={item.secret ? "password" : "text"}
|
||||
value={options(view(), group)[item.key] ?? ""}
|
||||
placeholder={item.placeholder}
|
||||
spellcheck={false}
|
||||
disabled={Boolean(ctx.saving())}
|
||||
onInput={(event) => providerField(group, item.key, event.currentTarget.value)}
|
||||
/>
|
||||
</FieldCard>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="ui-card agent-builder-card" padding={0}>
|
||||
<header class="ui-card-header">
|
||||
<div>
|
||||
<h2>Vector Store</h2>
|
||||
<p>Choose where indexed embeddings and metadata are stored.</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="ui-form agent-builder-form">
|
||||
<FieldCard
|
||||
label="Backend"
|
||||
description="LanceDB stores vectors locally by default. Qdrant connects to an external service."
|
||||
actions={
|
||||
<SourceBadge
|
||||
source={field("vectorStore")?.source}
|
||||
inherited={field("vectorStore")?.inherited}
|
||||
overridden={field("vectorStore")?.overridden}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CustomSelect
|
||||
class="indexing-select"
|
||||
label="Vector store"
|
||||
value={store()}
|
||||
options={stores}
|
||||
disabled={Boolean(ctx.saving())}
|
||||
onSelect={(value) => update({ vectorStore: value })}
|
||||
/>
|
||||
</FieldCard>
|
||||
|
||||
<Show
|
||||
when={store() === "qdrant"}
|
||||
fallback={
|
||||
<FieldCard
|
||||
label="LanceDB directory"
|
||||
description="Optional directory for local LanceDB storage."
|
||||
actions={
|
||||
<SourceBadge
|
||||
source={field("lancedb.directory")?.source}
|
||||
inherited={field("lancedb.directory")?.inherited}
|
||||
overridden={field("lancedb.directory")?.overridden}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<input
|
||||
value={view().lancedb?.directory ?? ""}
|
||||
placeholder="Default Kilo state directory"
|
||||
disabled={Boolean(ctx.saving())}
|
||||
onInput={(event) =>
|
||||
update({ lancedb: { ...draft().lancedb, directory: event.currentTarget.value || undefined } })
|
||||
}
|
||||
/>
|
||||
</FieldCard>
|
||||
}
|
||||
>
|
||||
<FieldCard
|
||||
label="Qdrant URL"
|
||||
description="Server URL for the Qdrant instance."
|
||||
actions={
|
||||
<SourceBadge
|
||||
source={field("qdrant.url")?.source}
|
||||
inherited={field("qdrant.url")?.inherited}
|
||||
overridden={field("qdrant.url")?.overridden}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<input
|
||||
value={view().qdrant?.url ?? ""}
|
||||
placeholder="http://localhost:6333"
|
||||
disabled={Boolean(ctx.saving())}
|
||||
onInput={(event) =>
|
||||
update({ qdrant: { ...draft().qdrant, url: event.currentTarget.value || undefined } })
|
||||
}
|
||||
/>
|
||||
</FieldCard>
|
||||
<FieldCard
|
||||
label="Qdrant API key"
|
||||
description="Optional API key for authenticated Qdrant instances."
|
||||
actions={
|
||||
<SourceBadge
|
||||
source={field("qdrant.apiKey")?.source}
|
||||
inherited={field("qdrant.apiKey")?.inherited}
|
||||
overridden={field("qdrant.apiKey")?.overridden}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
value={view().qdrant?.apiKey ?? ""}
|
||||
placeholder="Optional API key"
|
||||
disabled={Boolean(ctx.saving())}
|
||||
onInput={(event) =>
|
||||
update({ qdrant: { ...draft().qdrant, apiKey: event.currentTarget.value || undefined } })
|
||||
}
|
||||
/>
|
||||
</FieldCard>
|
||||
</Show>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="ui-card agent-builder-card" padding={0}>
|
||||
<header class="ui-card-header">
|
||||
<div>
|
||||
<h2>Search and Scanning</h2>
|
||||
<p>Tune result filtering, batching, and retries.</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="ui-form agent-builder-form">
|
||||
<FieldCard
|
||||
label="Minimum search score"
|
||||
description="Similarity threshold from 0 to 1. Default is model-specific or 0.4."
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={view().searchMinScore ?? ""}
|
||||
placeholder="0.4"
|
||||
disabled={Boolean(ctx.saving())}
|
||||
onInput={(event) => number("searchMinScore", event.currentTarget.value)}
|
||||
/>
|
||||
</FieldCard>
|
||||
<FieldCard
|
||||
label="Maximum search results"
|
||||
description="Maximum number of semantic matches returned per search."
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={view().searchMaxResults ?? ""}
|
||||
placeholder="50"
|
||||
disabled={Boolean(ctx.saving())}
|
||||
onInput={(event) => number("searchMaxResults", event.currentTarget.value)}
|
||||
/>
|
||||
</FieldCard>
|
||||
<FieldCard
|
||||
label="Embedding batch size"
|
||||
description="Number of code segments sent in each embedding batch."
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={view().embeddingBatchSize ?? ""}
|
||||
placeholder="60"
|
||||
disabled={Boolean(ctx.saving())}
|
||||
onInput={(event) => number("embeddingBatchSize", event.currentTarget.value)}
|
||||
/>
|
||||
</FieldCard>
|
||||
<FieldCard label="Scanner retry limit" description="Maximum retry attempts for a failed embedding batch.">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={view().scannerMaxBatchRetries ?? ""}
|
||||
placeholder="3"
|
||||
disabled={Boolean(ctx.saving())}
|
||||
onInput={(event) => number("scannerMaxBatchRetries", event.currentTarget.value)}
|
||||
/>
|
||||
</FieldCard>
|
||||
</div>
|
||||
<Show when={errors().length > 0}>
|
||||
<footer class="indexing-errors">
|
||||
<For each={errors()}>{(error) => <span>{error}</span>}</For>
|
||||
</footer>
|
||||
</Show>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
</ConfigPage>
|
||||
)
|
||||
}
|
||||
@@ -2,9 +2,8 @@ import { For, Show } from "solid-js"
|
||||
import { Button } from "@kilocode/kilo-web-ui/button"
|
||||
import { ConfigRow, SectionTitle } from "@kilocode/kilo-web-ui/console"
|
||||
import { IconButton } from "@kilocode/kilo-web-ui/icon-button"
|
||||
import { CountTag, Tag } from "@kilocode/kilo-web-ui/tag"
|
||||
import { SearchField } from "../../components/SearchField"
|
||||
import { ConfigPage, SourceBadge } from "./ConfigPage"
|
||||
import { ConfigCountTag as CountTag, ConfigPage, ConfigTag as Tag, SourceBadge } from "./ConfigPage"
|
||||
import { useKeybindSettings } from "./state/keybinds"
|
||||
|
||||
export function KeybindsRoute() {
|
||||
|
||||
@@ -2,12 +2,11 @@ import { For, Show } from "solid-js"
|
||||
import { Button } from "@kilocode/kilo-web-ui/button"
|
||||
import { Card } from "@kilocode/kilo-web-ui/card"
|
||||
import { IconButton } from "@kilocode/kilo-web-ui/icon-button"
|
||||
import { CountTag, Tag } from "@kilocode/kilo-web-ui/tag"
|
||||
import { StatusTag } from "@kilocode/kilo-web-ui/status-tag"
|
||||
import { ConfirmDialog } from "../../components/ConfirmDialog"
|
||||
import { CustomSelect, type SelectOption } from "../../components/CustomSelect"
|
||||
import { SearchField } from "../../components/SearchField"
|
||||
import { ConfigPage, SourceBadge } from "./ConfigPage"
|
||||
import { ConfigCountTag as CountTag, ConfigPage, ConfigTag as Tag, SourceBadge } from "./ConfigPage"
|
||||
import { useMcpSettings } from "./state/mcp"
|
||||
|
||||
type StatusFilter = "all" | "installed" | "notInstalled"
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { For, Show } from "solid-js"
|
||||
import { Button } from "@kilocode/kilo-web-ui/button"
|
||||
import { IconButton } from "@kilocode/kilo-web-ui/icon-button"
|
||||
import { Tag } from "@kilocode/kilo-web-ui/tag"
|
||||
import type { Model } from "@kilocode/sdk/v2/client"
|
||||
import { SearchField } from "../../components/SearchField"
|
||||
import { text } from "../../shared/utils"
|
||||
import { ConfigPage, SourceBadge } from "./ConfigPage"
|
||||
import { ConfigPage, ConfigTag as Tag, SourceBadge } from "./ConfigPage"
|
||||
import { type Capability, type ModelField, useModelSettings } from "./state/models"
|
||||
|
||||
function money(n: number) {
|
||||
|
||||
@@ -2,9 +2,8 @@ import { For, Show } from "solid-js"
|
||||
import { Button } from "@kilocode/kilo-web-ui/button"
|
||||
import { ConfigRow, SectionTitle } from "@kilocode/kilo-web-ui/console"
|
||||
import { IconButton } from "@kilocode/kilo-web-ui/icon-button"
|
||||
import { CountTag, Tag } from "@kilocode/kilo-web-ui/tag"
|
||||
import { toolName } from "../../shared/utils"
|
||||
import { ConfigPage, SourceBadge } from "./ConfigPage"
|
||||
import { ConfigCountTag as CountTag, ConfigPage, ConfigTag as Tag, SourceBadge } from "./ConfigPage"
|
||||
import { actions, usePermissionSettings, type PermissionAction, type PermissionRule } from "./state/permissions"
|
||||
|
||||
export function tone(action: PermissionAction) {
|
||||
|
||||
@@ -3,12 +3,11 @@ import { Button } from "@kilocode/kilo-web-ui/button"
|
||||
import { Card } from "@kilocode/kilo-web-ui/card"
|
||||
import { IconButton } from "@kilocode/kilo-web-ui/icon-button"
|
||||
import { ProviderIcon } from "@kilocode/kilo-web-ui/provider-icon"
|
||||
import { CountTag } from "@kilocode/kilo-web-ui/tag"
|
||||
import { StatusTag } from "@kilocode/kilo-web-ui/status-tag"
|
||||
import { ConfirmDialog } from "../../components/ConfirmDialog"
|
||||
import { CustomSelect } from "../../components/CustomSelect"
|
||||
import { SearchField } from "../../components/SearchField"
|
||||
import { ConfigPage, SourceBadge } from "./ConfigPage"
|
||||
import { ConfigCountTag as CountTag, ConfigPage, SourceBadge } from "./ConfigPage"
|
||||
import { useProviderSettings } from "./state/providers"
|
||||
|
||||
export function ProvidersRoute() {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Button } from "@kilocode/kilo-web-ui/button"
|
||||
import { Card } from "@kilocode/kilo-web-ui/card"
|
||||
import { Tag } from "@kilocode/kilo-web-ui/tag"
|
||||
import { For, Show, createMemo, createSignal } from "solid-js"
|
||||
import { useConfig } from "../../context/config"
|
||||
import { ConfigPage, ConfigToolbar } from "./ConfigPage"
|
||||
import { ConfigPage, ConfigTag as Tag, ConfigToolbar } from "./ConfigPage"
|
||||
|
||||
type Server = {
|
||||
id: string
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { For, Show } from "solid-js"
|
||||
import { Tag } from "@kilocode/kilo-web-ui/tag"
|
||||
import { useConfig } from "../../context/config"
|
||||
import { ConfigPage, ConfigToolbar } from "./ConfigPage"
|
||||
import { ConfigPage, ConfigTag as Tag, ConfigToolbar } from "./ConfigPage"
|
||||
|
||||
export function SourcesRoute() {
|
||||
const ctx = useConfig()
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||
import { ConfigRow, SectionTitle, StatusTag } from "@kilocode/kilo-web-ui/console"
|
||||
import { CountTag } from "@kilocode/kilo-web-ui/tag"
|
||||
import { SearchField } from "../../components/SearchField"
|
||||
import { useConfig } from "../../context/config"
|
||||
import { toolCapabilities, toolName } from "../../shared/utils"
|
||||
import { ConfigPage } from "./ConfigPage"
|
||||
import { ConfigCountTag as CountTag, ConfigPage } from "./ConfigPage"
|
||||
|
||||
export function ToolsRoute() {
|
||||
const ctx = useConfig()
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AgentBuilderRoute, AgentsRoute } from "./AgentsRoute"
|
||||
import { CliNotificationsRoute } from "./CliNotificationsRoute"
|
||||
import { CliUiRoute } from "./CliUiRoute"
|
||||
import { FormattersRoute, LspRoute } from "./FormattersRoute"
|
||||
import { IndexingRoute } from "./IndexingRoute"
|
||||
import { KeybindsRoute } from "./KeybindsRoute"
|
||||
import { McpRoute } from "./McpRoute"
|
||||
import { ModelsAvailableRoute, ModelsDefaultRoute, ModelsRoute } from "./ModelsRoute"
|
||||
@@ -92,7 +93,21 @@ export const configNav: ConfigNode[] = [
|
||||
{
|
||||
id: "behaviour",
|
||||
label: "Behaviour",
|
||||
items: [agents, tools, permissions, mcp, formatters, lsp],
|
||||
items: [
|
||||
agents,
|
||||
tools,
|
||||
permissions,
|
||||
mcp,
|
||||
formatters,
|
||||
lsp,
|
||||
{
|
||||
path: "/indexing",
|
||||
href: "/settings/indexing",
|
||||
icon: "circuit-board",
|
||||
label: "Code Indexing",
|
||||
component: IndexingRoute,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "cli",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { clean, merge, providerPatch, removed, shouldSync, validate } from "./indexing"
|
||||
|
||||
describe("indexing config state", () => {
|
||||
test("merges project settings over nested global settings", () => {
|
||||
expect(
|
||||
merge(
|
||||
{ enabled: true, provider: "openai", openai: { apiKey: "global" }, qdrant: { url: "http://global" } },
|
||||
{ enabled: false, provider: "ollama", qdrant: { apiKey: "project" } },
|
||||
),
|
||||
).toEqual({
|
||||
enabled: false,
|
||||
provider: "ollama",
|
||||
openai: { apiKey: "global" },
|
||||
qdrant: { url: "http://global", apiKey: "project" },
|
||||
})
|
||||
})
|
||||
|
||||
test("cleans empty fields and returns unset paths", () => {
|
||||
const before = {
|
||||
provider: "openai" as const,
|
||||
model: "text-embedding-3-small",
|
||||
openai: { apiKey: "secret" },
|
||||
}
|
||||
const after = clean({ provider: "openai", model: "", openai: { apiKey: "" } })
|
||||
|
||||
expect(after).toEqual({ provider: "openai" })
|
||||
expect(removed(before, after)).toEqual([
|
||||
["indexing", "model"],
|
||||
["indexing", "openai"],
|
||||
])
|
||||
})
|
||||
|
||||
test("resyncs dirty drafts when the scope changes", () => {
|
||||
expect(shouldSync("global", "global", true, "global", "updated")).toBe(false)
|
||||
expect(shouldSync("global", "project", true, "global", "project")).toBe(true)
|
||||
expect(shouldSync("global", "project", true, "shared", "shared")).toBe(true)
|
||||
})
|
||||
|
||||
test("builds provider patches for custom selector changes", () => {
|
||||
expect(providerPatch("kilo", "default-embedding")).toEqual({
|
||||
provider: "kilo",
|
||||
model: "default-embedding",
|
||||
dimension: undefined,
|
||||
})
|
||||
expect(providerPatch("ollama", "ignored")).toEqual({
|
||||
provider: "ollama",
|
||||
model: undefined,
|
||||
dimension: undefined,
|
||||
})
|
||||
expect(providerPatch("")).toEqual({ provider: undefined, model: undefined, dimension: undefined })
|
||||
})
|
||||
|
||||
test("validates numeric settings", () => {
|
||||
expect(validate({ dimension: 0, searchMinScore: 2, searchMaxResults: 0, embeddingBatchSize: 1.5 })).toEqual([
|
||||
"Vector dimension must be a positive integer.",
|
||||
"Search minimum score must be between 0 and 1.",
|
||||
"Search maximum results must be a positive integer.",
|
||||
"Embedding batch size must be a positive integer.",
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { IndexingConfig } from "@kilocode/sdk/v2/client"
|
||||
|
||||
function record(input: unknown): input is Record<string, unknown> {
|
||||
return typeof input === "object" && input !== null && !Array.isArray(input)
|
||||
}
|
||||
|
||||
export function clone(input: IndexingConfig | undefined): IndexingConfig {
|
||||
return structuredClone(input ?? {})
|
||||
}
|
||||
|
||||
export function shouldSync(selected: string, current: string, dirty: boolean, source: string, next: string) {
|
||||
return selected !== current || (!dirty && source !== next)
|
||||
}
|
||||
|
||||
export function merge(base: IndexingConfig | undefined, patch: IndexingConfig | undefined): IndexingConfig {
|
||||
const result: Record<string, unknown> = { ...(base ?? {}) }
|
||||
for (const [key, value] of Object.entries(patch ?? {})) {
|
||||
if (record(value) && record(result[key])) {
|
||||
result[key] = { ...result[key], ...value }
|
||||
continue
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
return result as IndexingConfig
|
||||
}
|
||||
|
||||
function prune(input: unknown): unknown {
|
||||
if (typeof input === "string") return input.trim() || undefined
|
||||
if (!record(input)) return input ?? undefined
|
||||
const entries = Object.entries(input).flatMap(([key, value]) => {
|
||||
const next = prune(value)
|
||||
return next === undefined ? [] : [[key, next] as const]
|
||||
})
|
||||
if (entries.length === 0) return undefined
|
||||
return Object.fromEntries(entries)
|
||||
}
|
||||
|
||||
export function clean(input: IndexingConfig): IndexingConfig {
|
||||
return (prune(input) ?? {}) as IndexingConfig
|
||||
}
|
||||
|
||||
export function providerPatch(provider: IndexingConfig["provider"] | "", model?: string): IndexingConfig {
|
||||
return {
|
||||
provider: provider || undefined,
|
||||
model: provider === "kilo" ? model || undefined : undefined,
|
||||
dimension: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function paths(before: unknown, after: unknown, prefix: string[]): string[][] {
|
||||
if (!record(before)) return []
|
||||
const next = record(after) ? after : {}
|
||||
return Object.entries(before).flatMap(([key, value]) => {
|
||||
const path = [...prefix, key]
|
||||
if (!(key in next)) return [path]
|
||||
if (record(value) && record(next[key])) return paths(value, next[key], path)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
export function removed(before: IndexingConfig, after: IndexingConfig): string[][] {
|
||||
return paths(before, after, ["indexing"])
|
||||
}
|
||||
|
||||
export function validate(input: IndexingConfig): string[] {
|
||||
const errors: string[] = []
|
||||
if (input.dimension !== undefined && input.dimension !== null) {
|
||||
if (!Number.isInteger(input.dimension) || input.dimension <= 0)
|
||||
errors.push("Vector dimension must be a positive integer.")
|
||||
}
|
||||
if (input.searchMinScore !== undefined && (input.searchMinScore < 0 || input.searchMinScore > 1)) {
|
||||
errors.push("Search minimum score must be between 0 and 1.")
|
||||
}
|
||||
const integers = [
|
||||
["Search maximum results", input.searchMaxResults],
|
||||
["Embedding batch size", input.embeddingBatchSize],
|
||||
["Scanner maximum retries", input.scannerMaxBatchRetries],
|
||||
] as const
|
||||
for (const [label, value] of integers) {
|
||||
if (value !== undefined && (!Number.isInteger(value) || value <= 0))
|
||||
errors.push(`${label} must be a positive integer.`)
|
||||
}
|
||||
return errors
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
@import "./styles/formatters.css";
|
||||
@import "./styles/keybinds.css";
|
||||
@import "./styles/models.css";
|
||||
@import "./styles/indexing.css";
|
||||
@import "./styles/agents-tools.css";
|
||||
@import "./styles/projects.css";
|
||||
@import "./styles/project-console.css";
|
||||
|
||||
@@ -180,8 +180,8 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.kilo-console .agent-builder-field-head span,
|
||||
.kilo-console .agent-builder-field > span {
|
||||
.kilo-console .agent-builder-field-head span:not([data-component="tag"]),
|
||||
.kilo-console .agent-builder-field > span:not([data-component="tag"]) {
|
||||
color: var(--foreground);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
.kilo-console .indexing-builder .agent-builder-card:has(.indexing-select[open]) {
|
||||
position: relative;
|
||||
z-index: 40;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.kilo-console .indexing-select[open] {
|
||||
z-index: 41;
|
||||
}
|
||||
|
||||
.kilo-console .indexing-toggle-tags {
|
||||
display: inline-flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.kilo-console .indexing-field-title {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.kilo-console .indexing-field-title > :first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kilo-console .indexing-note {
|
||||
grid-column: 1 / -1;
|
||||
border: 1px solid color-mix(in oklab, var(--primary) 35%, var(--border));
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in oklab, var(--primary) 8%, transparent);
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.5;
|
||||
padding: 0.625rem;
|
||||
}
|
||||
|
||||
.kilo-console .indexing-errors {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--destructive);
|
||||
font-size: 0.6875rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
@@ -15,6 +15,12 @@ export type KiloEmbeddingModelCatalog = {
|
||||
aliases: Record<string, string>
|
||||
}
|
||||
|
||||
export type KiloEmbeddingModelCatalogIssue = {
|
||||
code: "http" | "invalid-response" | "network"
|
||||
message: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
export const EMPTY_KILO_EMBEDDING_MODEL_CATALOG: KiloEmbeddingModelCatalog = {
|
||||
defaultModel: "",
|
||||
models: [],
|
||||
@@ -39,25 +45,66 @@ type Options = {
|
||||
baseURL?: string
|
||||
token?: string
|
||||
signal?: AbortSignal
|
||||
attempts?: number
|
||||
onError?: (issue: KiloEmbeddingModelCatalogIssue) => void
|
||||
}
|
||||
|
||||
const retryable = (status: number) => status === 408 || status === 425 || status === 429 || status >= 500
|
||||
|
||||
function wait(ms: number, signal?: AbortSignal) {
|
||||
if (signal?.aborted) return Promise.reject(signal.reason)
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const abort = () => {
|
||||
clearTimeout(timer)
|
||||
reject(signal?.reason)
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
signal?.removeEventListener("abort", abort)
|
||||
resolve()
|
||||
}, ms)
|
||||
signal?.addEventListener("abort", abort, { once: true })
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchKiloEmbeddingModelCatalog(options: Options = {}): Promise<KiloEmbeddingModelCatalog> {
|
||||
const url = new URL("embedding-models", resolveKiloGatewayBaseUrl({ baseURL: options.baseURL, token: options.token }))
|
||||
const requested = options.attempts ?? 3
|
||||
const attempts = Number.isFinite(requested) ? Math.min(3, Math.max(1, Math.floor(requested))) : 3
|
||||
const issue = { current: undefined as KiloEmbeddingModelCatalogIssue | undefined }
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { signal: options.signal })
|
||||
if (!response.ok) {
|
||||
console.warn(`[Kilo Gateway] Failed to fetch embedding model catalog: ${response.status}`)
|
||||
return EMPTY_KILO_EMBEDDING_MODEL_CATALOG
|
||||
for (const attempt of Array.from({ length: attempts }, (_, index) => index)) {
|
||||
if (options.signal?.aborted) throw options.signal.reason
|
||||
try {
|
||||
const response = await fetch(url, { signal: options.signal, redirect: "error" })
|
||||
if (!response.ok) {
|
||||
issue.current = {
|
||||
code: "http",
|
||||
message: `Unable to load Kilo embedding models (HTTP ${response.status}).`,
|
||||
status: response.status,
|
||||
}
|
||||
if (!retryable(response.status) || attempt === attempts - 1) break
|
||||
await wait(200 * 2 ** attempt, options.signal)
|
||||
continue
|
||||
}
|
||||
const body = await response.json().catch(() => undefined)
|
||||
const parsed = catalog.safeParse(body)
|
||||
if (parsed.success) return parsed.data
|
||||
issue.current = {
|
||||
code: "invalid-response",
|
||||
message: "Kilo returned an invalid embedding model catalog.",
|
||||
}
|
||||
break
|
||||
} catch (err) {
|
||||
if (options.signal?.aborted) throw options.signal.reason
|
||||
issue.current = {
|
||||
code: "network",
|
||||
message: "Unable to connect to Kilo to load embedding models. Check your network connection and try again.",
|
||||
}
|
||||
if (attempt === attempts - 1) break
|
||||
await wait(200 * 2 ** attempt, options.signal)
|
||||
}
|
||||
const parsed = catalog.safeParse(await response.json())
|
||||
if (!parsed.success) {
|
||||
console.warn("[Kilo Gateway] Embedding model catalog response validation failed:", parsed.error.format())
|
||||
return EMPTY_KILO_EMBEDDING_MODEL_CATALOG
|
||||
}
|
||||
return parsed.data
|
||||
} catch (err) {
|
||||
console.warn("[Kilo Gateway] Error fetching embedding model catalog:", err)
|
||||
return EMPTY_KILO_EMBEDDING_MODEL_CATALOG
|
||||
}
|
||||
|
||||
if (issue.current) options.onError?.(issue.current)
|
||||
return EMPTY_KILO_EMBEDDING_MODEL_CATALOG
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export {
|
||||
fetchKiloEmbeddingModelCatalog,
|
||||
type KiloEmbeddingModel,
|
||||
type KiloEmbeddingModelCatalog,
|
||||
type KiloEmbeddingModelCatalogIssue,
|
||||
} from "./api/embedding-models.js"
|
||||
export { resolveKiloGatewayBaseUrl, resolveKiloOpenRouterBaseUrl } from "./api/url.js"
|
||||
export {
|
||||
|
||||
@@ -1,43 +1,81 @@
|
||||
import { describe, expect, mock, test } from "bun:test"
|
||||
import { describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import { EMPTY_KILO_EMBEDDING_MODEL_CATALOG, fetchKiloEmbeddingModelCatalog } from "../../src/api/embedding-models"
|
||||
|
||||
const response = () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
defaultModel: "provider/model",
|
||||
models: [{ id: "provider/model", name: "Provider Model", dimension: 1024, scoreThreshold: 0.4 }],
|
||||
aliases: { model: "provider/model" },
|
||||
}),
|
||||
)
|
||||
|
||||
describe("fetchKiloEmbeddingModelCatalog", () => {
|
||||
test("fetches catalog from Kilo Gateway", async () => {
|
||||
const prev = global.fetch
|
||||
const fn = mock(() =>
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
defaultModel: "provider/model",
|
||||
models: [{ id: "provider/model", name: "Provider Model", dimension: 1024, scoreThreshold: 0.4 }],
|
||||
aliases: { model: "provider/model" },
|
||||
}),
|
||||
),
|
||||
),
|
||||
) as unknown as typeof fetch
|
||||
const fn = mock(() => Promise.resolve(response())) as unknown as typeof fetch
|
||||
global.fetch = fn
|
||||
|
||||
try {
|
||||
const catalog = await fetchKiloEmbeddingModelCatalog({ baseURL: "https://example.test" })
|
||||
|
||||
expect(catalog.defaultModel).toBe("provider/model")
|
||||
expect((fn as unknown as { mock: { calls: Array<[URL]> } }).mock.calls[0]?.[0].toString()).toBe(
|
||||
"https://example.test/api/gateway/embedding-models",
|
||||
)
|
||||
const call = (fn as unknown as { mock: { calls: Array<[URL, RequestInit]> } }).mock.calls[0]
|
||||
expect(call?.[0].toString()).toBe("https://example.test/api/gateway/embedding-models")
|
||||
expect(call?.[1].redirect).toBe("error")
|
||||
} finally {
|
||||
global.fetch = prev
|
||||
}
|
||||
})
|
||||
|
||||
test("falls back when the request fails", async () => {
|
||||
test("retries transient transport failures", async () => {
|
||||
const prev = global.fetch
|
||||
const fn = mock(() => Promise.reject(new TypeError("fetch failed")))
|
||||
fn.mockImplementationOnce(() => Promise.reject(new TypeError("fetch failed")))
|
||||
fn.mockImplementationOnce(() => Promise.resolve(response()))
|
||||
global.fetch = fn as unknown as typeof fetch
|
||||
|
||||
try {
|
||||
const catalog = await fetchKiloEmbeddingModelCatalog({ baseURL: "https://example.test" })
|
||||
|
||||
expect(catalog.models).toHaveLength(1)
|
||||
expect(fn).toHaveBeenCalledTimes(2)
|
||||
} finally {
|
||||
global.fetch = prev
|
||||
}
|
||||
})
|
||||
|
||||
test("bounds caller-controlled retry attempts", async () => {
|
||||
const prev = global.fetch
|
||||
const fn = mock(() => Promise.resolve(new Response("nope", { status: 500 })))
|
||||
global.fetch = fn as unknown as typeof fetch
|
||||
|
||||
try {
|
||||
await fetchKiloEmbeddingModelCatalog({ baseURL: "https://example.test", attempts: Number.POSITIVE_INFINITY })
|
||||
expect(fn).toHaveBeenCalledTimes(3)
|
||||
} finally {
|
||||
global.fetch = prev
|
||||
}
|
||||
})
|
||||
|
||||
test("reports a final failure without writing to the console", async () => {
|
||||
const prev = global.fetch
|
||||
const warn = spyOn(console, "warn").mockImplementation(() => undefined)
|
||||
const issue = mock(() => undefined)
|
||||
global.fetch = mock(() => Promise.resolve(new Response("nope", { status: 500 }))) as unknown as typeof fetch
|
||||
|
||||
try {
|
||||
await expect(fetchKiloEmbeddingModelCatalog({ baseURL: "https://example.test" })).resolves.toEqual(
|
||||
EMPTY_KILO_EMBEDDING_MODEL_CATALOG,
|
||||
)
|
||||
await expect(
|
||||
fetchKiloEmbeddingModelCatalog({ baseURL: "https://example.test", attempts: 1, onError: issue }),
|
||||
).resolves.toEqual(EMPTY_KILO_EMBEDDING_MODEL_CATALOG)
|
||||
expect(issue).toHaveBeenCalledWith({
|
||||
code: "http",
|
||||
message: "Unable to load Kilo embedding models (HTTP 500).",
|
||||
status: 500,
|
||||
})
|
||||
expect(warn).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
warn.mockRestore()
|
||||
global.fetch = prev
|
||||
}
|
||||
})
|
||||
|
||||
@@ -993,7 +993,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
)
|
||||
break
|
||||
case "updateConfig":
|
||||
await this.handleUpdateConfig(message.config, message.projectConfig)
|
||||
await this.handleUpdateConfig(
|
||||
message.config,
|
||||
message.projectConfig,
|
||||
message.globalUnset,
|
||||
message.projectUnset,
|
||||
)
|
||||
break
|
||||
case "openSettingsTab":
|
||||
if (message.tab === "indexing") {
|
||||
@@ -2062,16 +2067,18 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
|
||||
try {
|
||||
const workspaceDir = this.getWorkspaceDirectory()
|
||||
const { data: config } = await retry(() =>
|
||||
this.client!.config.get({ directory: workspaceDir }, { throwOnError: true }),
|
||||
)
|
||||
const { data: global } = await this.client.global.config.get({ throwOnError: true })
|
||||
const [{ data: config }, { data: global }, { data: overlay }] = await Promise.all([
|
||||
retry(() => this.client!.config.get({ directory: workspaceDir }, { throwOnError: true })),
|
||||
this.client.global.config.get({ throwOnError: true }),
|
||||
this.client.config.overlay({ directory: workspaceDir, scope: "project" }, { throwOnError: true }),
|
||||
])
|
||||
this.cachedGlobalConfig = global ?? null
|
||||
|
||||
const message = {
|
||||
type: "configLoaded",
|
||||
config,
|
||||
globalConfig: global,
|
||||
projectConfig: overlay?.project,
|
||||
features: configFeatures(config),
|
||||
}
|
||||
this.cachedConfigMessage = message
|
||||
@@ -2156,16 +2163,26 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
if (!this.client || this.connectionState !== "connected") return
|
||||
try {
|
||||
const dir = this.getWorkspaceDirectory()
|
||||
const { data: config } = await retry(() => this.client!.config.get({ directory: dir }, { throwOnError: true }))
|
||||
const { data: global } = await this.client.global.config.get({ throwOnError: true })
|
||||
const [{ data: config }, { data: global }, { data: overlay }] = await Promise.all([
|
||||
retry(() => this.client!.config.get({ directory: dir }, { throwOnError: true })),
|
||||
this.client.global.config.get({ throwOnError: true }),
|
||||
this.client.config.overlay({ directory: dir, scope: "project" }, { throwOnError: true }),
|
||||
])
|
||||
this.cachedGlobalConfig = global ?? null
|
||||
this.cachedConfigMessage = {
|
||||
type: "configLoaded",
|
||||
config,
|
||||
globalConfig: global,
|
||||
projectConfig: overlay?.project,
|
||||
features: configFeatures(config),
|
||||
}
|
||||
this.postMessage({ type: "configUpdated", config, globalConfig: global, features: configFeatures(config) })
|
||||
this.postMessage({
|
||||
type: "configUpdated",
|
||||
config,
|
||||
globalConfig: global,
|
||||
projectConfig: overlay?.project,
|
||||
features: configFeatures(config),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: Failed to fetch config after update:", error)
|
||||
}
|
||||
@@ -2321,7 +2338,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
return getBusySessionCount(this.sessionStatusMap)
|
||||
}
|
||||
|
||||
private async handleUpdateConfig(partial: Partial<Config>, project: Partial<Config> = {}): Promise<void> {
|
||||
private async handleUpdateConfig(
|
||||
partial: Partial<Config>,
|
||||
project: Partial<Config> = {},
|
||||
globalUnset: string[][] = [],
|
||||
projectUnset: string[][] = [],
|
||||
): Promise<void> {
|
||||
if (!this.client || this.connectionState !== "connected") {
|
||||
this.postMessage({ type: "configUpdateFailed", message: "Not connected to CLI backend" })
|
||||
return
|
||||
@@ -2336,16 +2358,26 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
partial.agent !== undefined ||
|
||||
project.default_agent !== undefined ||
|
||||
project.agent !== undefined
|
||||
const hasGlobal = Object.keys(partial).length > 0
|
||||
const hasProject = Object.keys(project).length > 0
|
||||
const hasGlobal = Object.keys(partial).length > 0 || globalUnset.length > 0
|
||||
const hasProject = Object.keys(project).length > 0 || projectUnset.length > 0
|
||||
|
||||
this.pending++
|
||||
const dir = this.getWorkspaceDirectory()
|
||||
|
||||
try {
|
||||
await this.connectionService.drainPendingPrompts()
|
||||
if (hasGlobal) await this.client.global.config.update({ config: partial }, { throwOnError: true })
|
||||
if (hasProject) await this.client.config.update({ config: project, directory: dir }, { throwOnError: true })
|
||||
if (hasGlobal) {
|
||||
await this.client.config.overlayUpdate(
|
||||
{ scope: "global", set: partial, unset: globalUnset, directory: dir },
|
||||
{ throwOnError: true },
|
||||
)
|
||||
}
|
||||
if (hasProject) {
|
||||
await this.client.config.overlayUpdate(
|
||||
{ scope: "project", set: project, unset: projectUnset, directory: dir },
|
||||
{ throwOnError: true },
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
this.postConfigFailure(error)
|
||||
this.pending--
|
||||
@@ -2353,19 +2385,24 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: merged } = await retry(() => this.client!.config.get({ directory: dir }, { throwOnError: true }))
|
||||
const { data: global } = await this.client.global.config.get({ throwOnError: true })
|
||||
const [{ data: merged }, { data: global }, { data: overlay }] = await Promise.all([
|
||||
retry(() => this.client!.config.get({ directory: dir }, { throwOnError: true })),
|
||||
this.client.global.config.get({ throwOnError: true }),
|
||||
this.client.config.overlay({ directory: dir, scope: "project" }, { throwOnError: true }),
|
||||
])
|
||||
this.cachedGlobalConfig = global ?? null
|
||||
this.cachedConfigMessage = {
|
||||
type: "configLoaded",
|
||||
config: merged,
|
||||
globalConfig: global,
|
||||
projectConfig: overlay?.project,
|
||||
features: configFeatures(merged),
|
||||
}
|
||||
this.postMessage({
|
||||
type: "configUpdated",
|
||||
config: merged,
|
||||
globalConfig: global,
|
||||
projectConfig: overlay?.project,
|
||||
features: configFeatures(merged),
|
||||
})
|
||||
await Promise.all([
|
||||
|
||||
@@ -718,5 +718,5 @@ export function isEventFromForeignProject(event: StreamEvent, expectedProjectID:
|
||||
}
|
||||
if (event.name !== "session.updated.1") return false
|
||||
const project = event.data.info.projectID
|
||||
return project != null && project !== expectedProjectID
|
||||
return project !== undefined && project !== expectedProjectID
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ const GLOBALS = "colorScheme:dark;theme:kilo-vscode;vscodeTheme:dark-modern"
|
||||
const STORY_ID = "settings--indexing-provider-blur-race"
|
||||
const KILO_STORY_ID = "settings--indexing-kilo-model-preset"
|
||||
const KILO_LOADING_STORY_ID = "settings--indexing-kilo-catalog-loading"
|
||||
const SCOPE_STORY_ID = "settings--indexing-scope-switch"
|
||||
|
||||
type Saved = {
|
||||
provider?: string
|
||||
@@ -19,6 +20,7 @@ type Saved = {
|
||||
dimension?: number | null
|
||||
openai?: { apiKey?: string }
|
||||
gemini?: { apiKey?: string }
|
||||
qdrant?: { url?: string; apiKey?: string }
|
||||
}
|
||||
|
||||
function storyUrl(id = STORY_ID) {
|
||||
@@ -76,6 +78,41 @@ test("provider switch writes to selected provider bucket", async ({ page }) => {
|
||||
await expect(model).toHaveAttribute("placeholder", "Enter model ID")
|
||||
})
|
||||
|
||||
test("scope switching preserves raw overrides and commits blur to the original scope", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 420, height: 720 })
|
||||
await page.goto(storyUrl(SCOPE_STORY_ID), { waitUntil: "load" })
|
||||
await disableAnimations(page)
|
||||
await page.waitForSelector("#storybook-root *", { state: "attached" })
|
||||
|
||||
await expect(page.locator('[data-slot="settings-row"] [data-component="tag"]')).toHaveCount(0)
|
||||
|
||||
const url = field(page, "Qdrant URL").first()
|
||||
await url.fill("http://edited-global:6333")
|
||||
await page.getByRole("button", { name: "Local", exact: true }).click()
|
||||
|
||||
const global = page.getByTestId("indexing-global-save")
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const cfg = JSON.parse(((await global.textContent()) ?? "{}").trim()) as Saved
|
||||
return cfg.qdrant?.url
|
||||
})
|
||||
.toBe("http://edited-global:6333")
|
||||
|
||||
const project = JSON.parse(((await page.getByTestId("indexing-project-save").textContent()) ?? "{}").trim()) as Saved
|
||||
expect(project.qdrant?.url).toBeUndefined()
|
||||
|
||||
await expect(field(page, "Embedding model").first()).toHaveValue("")
|
||||
await expect(url).toHaveValue("http://edited-global:6333")
|
||||
const urlRow = page.locator('[data-slot="settings-row"]', { hasText: "Qdrant URL" })
|
||||
const keyRow = page.locator('[data-slot="settings-row"]', { hasText: "Qdrant API key" })
|
||||
const modelRow = page.locator('[data-slot="settings-row"]', { hasText: "Embedding model" })
|
||||
const tuningRow = page.locator('[data-slot="settings-row"]', { hasText: "Search max results" })
|
||||
await expect(urlRow.locator('[data-component="tag"]')).toHaveText("Global")
|
||||
await expect(keyRow.locator('[data-component="tag"]')).toHaveText("Local")
|
||||
await expect(modelRow.locator('[data-component="tag"]')).toHaveText("Local")
|
||||
await expect(tuningRow.locator('[data-component="tag"]')).toHaveText("Default")
|
||||
})
|
||||
|
||||
test("Kilo exposes only supported embedding model presets", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 420, height: 720 })
|
||||
await page.goto(storyUrl(KILO_STORY_ID), { waitUntil: "load" })
|
||||
@@ -110,11 +147,18 @@ test("enabling Kilo before its catalog loads does not store an empty model", asy
|
||||
await page.goto(storyUrl(KILO_LOADING_STORY_ID), { waitUntil: "load" })
|
||||
await disableAnimations(page)
|
||||
await page.waitForSelector("#storybook-root *", { state: "attached" })
|
||||
await page.locator('[data-component="switch"] [data-slot="switch-control"]').nth(1).click()
|
||||
await page.getByRole("button", { name: "Local", exact: true }).click()
|
||||
await page
|
||||
.locator('[data-slot="settings-row"]', { hasText: "Enable for this project" })
|
||||
.locator('[data-slot="switch-control"]')
|
||||
.click()
|
||||
await verify()
|
||||
|
||||
await page.goto(storyUrl(KILO_LOADING_STORY_ID), { waitUntil: "load" })
|
||||
await page.waitForSelector("#storybook-root *", { state: "attached" })
|
||||
await page.locator('[data-component="switch"] [data-slot="switch-control"]').first().click()
|
||||
await page
|
||||
.locator('[data-slot="settings-row"]', { hasText: "Enable globally" })
|
||||
.locator('[data-slot="switch-control"]')
|
||||
.click()
|
||||
await verify()
|
||||
})
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { deepMerge, stripNulls, ConfigState } from "../../webview-ui/src/utils/config-utils"
|
||||
import {
|
||||
configUnsetPaths,
|
||||
ConfigState,
|
||||
deepMerge,
|
||||
mergeScopedConfig,
|
||||
pruneConfigSet,
|
||||
stripNulls,
|
||||
} from "../../webview-ui/src/utils/config-utils"
|
||||
import type { Config } from "../../webview-ui/src/types/messages"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -42,6 +49,38 @@ describe("deepMerge", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("scoped config normalization", () => {
|
||||
it("preserves indexing null overrides while stripping unrelated nulls", () => {
|
||||
const target = { username: "alice", indexing: { model: "global", dimension: 1024 } } as Config
|
||||
const source = { username: null, indexing: { model: null, dimension: null } } as unknown as Partial<Config>
|
||||
|
||||
expect(mergeScopedConfig(target, source)).toEqual({ indexing: { model: null, dimension: null } })
|
||||
})
|
||||
|
||||
it("builds clean set and unset payloads while preserving indexing null overrides", () => {
|
||||
const patch = {
|
||||
formatter: {},
|
||||
username: null,
|
||||
indexing: {
|
||||
model: null,
|
||||
dimension: null,
|
||||
searchMinScore: undefined,
|
||||
qdrant: { apiKey: undefined },
|
||||
},
|
||||
}
|
||||
|
||||
expect(pruneConfigSet(patch)).toEqual({
|
||||
formatter: {},
|
||||
indexing: { model: null, dimension: null },
|
||||
})
|
||||
expect(configUnsetPaths(patch)).toEqual([
|
||||
["username"],
|
||||
["indexing", "searchMinScore"],
|
||||
["indexing", "qdrant", "apiKey"],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("stripNulls", () => {
|
||||
it("removes null values", () => {
|
||||
const cfg = { snapshot: true, username: null } as unknown as Config
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import {
|
||||
indexingConfig,
|
||||
indexingDescription,
|
||||
indexingEnabled,
|
||||
indexingEnabledInherited,
|
||||
indexingInheritance,
|
||||
indexingSource,
|
||||
indexingUpdate,
|
||||
} from "../../webview-ui/src/components/settings/indexing-tab-state"
|
||||
|
||||
describe("indexing tab scope state", () => {
|
||||
it("uses the global value when project enablement is inherited", () => {
|
||||
expect(indexingEnabled("project", { enabled: true }, {})).toBe(true)
|
||||
expect(indexingEnabled("project", { enabled: false }, {})).toBe(false)
|
||||
expect(indexingEnabledInherited("project", { enabled: true }, {})).toBe(true)
|
||||
expect(indexingEnabledInherited("project", { enabled: false }, {})).toBe(true)
|
||||
})
|
||||
|
||||
it("uses explicit project overrides", () => {
|
||||
expect(indexingEnabled("project", { enabled: true }, { enabled: false })).toBe(false)
|
||||
expect(indexingEnabled("project", { enabled: false }, { enabled: true })).toBe(true)
|
||||
expect(indexingEnabledInherited("project", { enabled: true }, { enabled: false })).toBe(false)
|
||||
})
|
||||
|
||||
it("ignores project values in global scope", () => {
|
||||
const global = { enabled: false, provider: "openai" as const, openai: { apiKey: "global" } }
|
||||
const project = { enabled: true, provider: "ollama" as const, ollama: { baseUrl: "http://project" } }
|
||||
|
||||
expect(indexingEnabled("global", global, project)).toBe(false)
|
||||
expect(indexingEnabledInherited("global", global, {})).toBe(false)
|
||||
expect(indexingConfig("global", global, project)).toEqual(global)
|
||||
})
|
||||
|
||||
it("keeps inherited values out of project updates", () => {
|
||||
expect(
|
||||
indexingUpdate(
|
||||
"project",
|
||||
{ enabled: true, provider: "openai", openai: { apiKey: "global" } },
|
||||
{ qdrant: { url: "http://project" } },
|
||||
{ enabled: false },
|
||||
),
|
||||
).toEqual({ enabled: false, qdrant: { url: "http://project" } })
|
||||
})
|
||||
|
||||
it("preserves explicit null overrides and recursively inherits undefined leaves", () => {
|
||||
expect(
|
||||
indexingConfig(
|
||||
"project",
|
||||
{
|
||||
model: "global-model",
|
||||
dimension: 1024,
|
||||
qdrant: { url: "http://global", apiKey: "global-secret" },
|
||||
},
|
||||
{
|
||||
model: null,
|
||||
dimension: null,
|
||||
qdrant: { url: "http://project", apiKey: undefined },
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
model: null,
|
||||
dimension: null,
|
||||
qdrant: { url: "http://project", apiKey: "global-secret" },
|
||||
})
|
||||
})
|
||||
|
||||
it("classifies inherited and partially inherited fields", () => {
|
||||
const global = {
|
||||
provider: "openai-compatible" as const,
|
||||
model: "global-model",
|
||||
dimension: 1024,
|
||||
"openai-compatible": { baseUrl: "https://global.test", apiKey: "secret" },
|
||||
}
|
||||
const project = {
|
||||
model: null,
|
||||
"openai-compatible": { baseUrl: "https://project.test" },
|
||||
}
|
||||
|
||||
expect(indexingInheritance("project", global, project, [["provider"]])).toBe("inherited")
|
||||
expect(indexingInheritance("project", global, project, [["model"]])).toBe("none")
|
||||
expect(indexingInheritance("project", global, project, [["dimension"]])).toBe("inherited")
|
||||
expect(
|
||||
indexingInheritance("project", global, project, [
|
||||
["openai-compatible", "baseUrl"],
|
||||
["openai-compatible", "apiKey"],
|
||||
]),
|
||||
).toBe("partial")
|
||||
expect(indexingInheritance("global", global, project, [["provider"]])).toBe("none")
|
||||
expect(indexingInheritance("project", {}, {}, [["vectorStore"]])).toBe("none")
|
||||
expect(indexingSource("project", global, project, [["provider"]])).toBe("global")
|
||||
expect(indexingSource("project", global, project, [["model"]])).toBe("local")
|
||||
expect(
|
||||
indexingSource("project", global, project, [
|
||||
["openai-compatible", "baseUrl"],
|
||||
["openai-compatible", "apiKey"],
|
||||
]),
|
||||
).toBe("mixed")
|
||||
expect(indexingSource("project", {}, {}, [["vectorStore"]])).toBe("default")
|
||||
expect(indexingSource("global", global, project, [["provider"]])).toBe("none")
|
||||
expect(indexingDescription("Configure this value.", "inherited")).toBe(
|
||||
"Configure this value. Inherited from global config.",
|
||||
)
|
||||
})
|
||||
|
||||
it("merges inherited values with project overrides", () => {
|
||||
expect(
|
||||
indexingConfig(
|
||||
"project",
|
||||
{
|
||||
enabled: true,
|
||||
provider: "openai",
|
||||
model: "global-model",
|
||||
vectorStore: "qdrant",
|
||||
openai: { apiKey: "global" },
|
||||
qdrant: { url: "http://global", apiKey: "global-secret" },
|
||||
},
|
||||
{ provider: "ollama", qdrant: { url: "http://project" } },
|
||||
),
|
||||
).toEqual({
|
||||
enabled: true,
|
||||
provider: "ollama",
|
||||
model: "global-model",
|
||||
vectorStore: "qdrant",
|
||||
openai: { apiKey: "global" },
|
||||
qdrant: { url: "http://project", apiKey: "global-secret" },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -10,7 +10,12 @@ type Internals = {
|
||||
cachedIndexingStatusMessage: unknown
|
||||
handleEvent: (event: unknown, directory?: string) => void
|
||||
reloadAfterAuthChange: () => Promise<void>
|
||||
handleUpdateConfig: (partial: Partial<Config>) => Promise<void>
|
||||
handleUpdateConfig: (
|
||||
partial: Partial<Config>,
|
||||
project?: Partial<Config>,
|
||||
globalUnset?: string[][],
|
||||
projectUnset?: string[][],
|
||||
) => Promise<void>
|
||||
fetchAndSendConfig: () => Promise<void>
|
||||
fetchAndSendProviders: () => Promise<void>
|
||||
fetchAndSendAgents: () => Promise<void>
|
||||
@@ -22,6 +27,7 @@ type Internals = {
|
||||
|
||||
function createConnection() {
|
||||
let drains = 0
|
||||
const patches: unknown[] = []
|
||||
const client = {
|
||||
global: {
|
||||
config: {
|
||||
@@ -32,11 +38,17 @@ function createConnection() {
|
||||
config: {
|
||||
get: async () => ({ data: {} }),
|
||||
update: async () => ({ data: {} }),
|
||||
overlay: async () => ({ data: { project: {} } }),
|
||||
overlayUpdate: async (patch: unknown) => {
|
||||
patches.push(patch)
|
||||
return { data: {} }
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
drains: () => drains,
|
||||
patches: () => patches,
|
||||
service: {
|
||||
drainPendingPrompts: async () => {
|
||||
drains += 1
|
||||
@@ -97,6 +109,33 @@ describe("KiloProvider indexing refresh", () => {
|
||||
expect(indexing).toBe(0)
|
||||
})
|
||||
|
||||
it("passes scoped unset paths to the config overlay endpoint", async () => {
|
||||
const conn = createConnection()
|
||||
const provider = new KiloProvider({} as never, conn.service as never)
|
||||
const internal = provider as unknown as Internals
|
||||
internal.connectionState = "connected"
|
||||
|
||||
await internal.handleUpdateConfig(
|
||||
{ indexing: { qdrant: { apiKey: undefined } } },
|
||||
{ indexing: { searchMinScore: undefined } },
|
||||
[["indexing", "qdrant", "apiKey"]],
|
||||
[["indexing", "searchMinScore"]],
|
||||
)
|
||||
|
||||
expect(conn.patches()).toEqual([
|
||||
expect.objectContaining({
|
||||
scope: "global",
|
||||
set: { indexing: { qdrant: { apiKey: undefined } } },
|
||||
unset: [["indexing", "qdrant", "apiKey"]],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
scope: "project",
|
||||
set: { indexing: { searchMinScore: undefined } },
|
||||
unset: [["indexing", "searchMinScore"]],
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("fetchAndSendIndexingStatus uses current session directory header", async () => {
|
||||
const worktree = "/repo/.kilo/.kilocode/worktrees/feature"
|
||||
const calls: { input: RequestInfo | URL; init?: RequestInit }[] = []
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Component, For, Show, createMemo, createSignal } from "solid-js"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { Card } from "@kilocode/kilo-ui/card"
|
||||
import { DEFAULT_VECTOR_STORE } from "@kilocode/kilo-indexing/config"
|
||||
import { formatKiloEmbeddingModelLabel, getKiloEmbeddingModel } from "@kilocode/kilo-indexing/embedding-models"
|
||||
import { Select } from "@kilocode/kilo-ui/select"
|
||||
import { Switch } from "@kilocode/kilo-ui/switch"
|
||||
import { TextField } from "@kilocode/kilo-ui/text-field"
|
||||
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
|
||||
import { useConfig } from "../../context/config"
|
||||
import { formatIndexingLabel, useIndexing } from "../../context/indexing"
|
||||
import { useKiloEmbeddingModels } from "../../context/kilo-embedding-models"
|
||||
@@ -15,6 +15,17 @@ import { useServer } from "../../context/server"
|
||||
import type { IndexingConfig, IndexingProvider as ProviderId } from "../../types/messages"
|
||||
import { KILO_PROVIDER_ID } from "../../../../src/shared/provider-model"
|
||||
import SettingsRow from "./SettingsRow"
|
||||
import {
|
||||
indexingConfig,
|
||||
indexingDescription,
|
||||
indexingEnabled,
|
||||
indexingEnabledInherited,
|
||||
indexingInheritance,
|
||||
indexingSource,
|
||||
indexingUpdate,
|
||||
type IndexingScope,
|
||||
type IndexingSource,
|
||||
} from "./indexing-tab-state"
|
||||
|
||||
type Option = { value: string; label: string }
|
||||
type TuningKey = "searchMinScore" | "searchMaxResults" | "embeddingBatchSize" | "scannerMaxBatchRetries"
|
||||
@@ -44,6 +55,14 @@ const tuning: Array<{ key: TuningKey; label: string; placeholder: string }> = [
|
||||
{ key: "scannerMaxBatchRetries", label: "Scanner Max Batch Retries", placeholder: "3" },
|
||||
]
|
||||
|
||||
function sourceLabel(source: IndexingSource) {
|
||||
if (source === "global") return "Global"
|
||||
if (source === "local") return "Local"
|
||||
if (source === "mixed") return "Global + Local"
|
||||
if (source === "default") return "Default"
|
||||
return ""
|
||||
}
|
||||
|
||||
function providerFields(provider: ProviderId | undefined): Array<{ key: string; label: string; placeholder: string }> {
|
||||
if (provider === "kilo") return []
|
||||
if (provider === "openai") return [{ key: "apiKey", label: "API Key", placeholder: "sk-..." }]
|
||||
@@ -74,7 +93,7 @@ function providerFields(provider: ProviderId | undefined): Array<{ key: string;
|
||||
}
|
||||
|
||||
const IndexingTab: Component = () => {
|
||||
const { config, globalConfig, updateConfig, updateGlobalConfig } = useConfig()
|
||||
const { globalConfig, projectConfig, updateGlobalConfig, updateProjectConfig } = useConfig()
|
||||
const indexing = useIndexing()
|
||||
const embeds = useKiloEmbeddingModels()
|
||||
const language = useLanguage()
|
||||
@@ -83,13 +102,33 @@ const IndexingTab: Component = () => {
|
||||
const [providerDrafts, setProviderDrafts] = createSignal<Record<string, string>>({})
|
||||
const [storeDrafts, setStoreDrafts] = createSignal<Record<string, string>>({})
|
||||
const [tuningDrafts, setTuningDrafts] = createSignal<Record<string, string>>({})
|
||||
const [scope, setScope] = createSignal<IndexingScope>("global")
|
||||
|
||||
const cfg = createMemo<IndexingConfig>(() => config().indexing ?? {})
|
||||
const globalCfg = createMemo<IndexingConfig>(() => globalConfig().indexing ?? {})
|
||||
const globalOn = createMemo(() => globalCfg().enabled === true)
|
||||
const projectCfg = createMemo<IndexingConfig>(() => projectConfig().indexing ?? {})
|
||||
const raw = createMemo<IndexingConfig>(() => (scope() === "global" ? globalCfg() : projectCfg()))
|
||||
const cfg = createMemo<IndexingConfig>(() => indexingConfig(scope(), globalCfg(), projectCfg()))
|
||||
const enabled = createMemo(() => indexingEnabled(scope(), globalCfg(), projectCfg()))
|
||||
const inherited = createMemo(() => indexingEnabledInherited(scope(), globalCfg(), projectCfg()))
|
||||
const inheritance = (paths: readonly (readonly string[])[]) =>
|
||||
indexingInheritance(scope(), globalCfg(), projectCfg(), paths)
|
||||
const tag = (current: IndexingScope, paths: readonly (readonly string[])[]) =>
|
||||
sourceLabel(indexingSource(current, globalCfg(), projectCfg(), paths)) || undefined
|
||||
const description = (value: string, paths: readonly (readonly string[])[]) =>
|
||||
indexingDescription(value, inheritance(paths))
|
||||
const changeScope = (next: IndexingScope) => {
|
||||
const active = document.activeElement
|
||||
if (active instanceof HTMLElement) active.blur()
|
||||
setScope(next)
|
||||
}
|
||||
|
||||
const updateIndexing = (partial: IndexingConfig) => {
|
||||
updateConfig({ indexing: { ...cfg(), ...partial } })
|
||||
const patch = { indexing: indexingUpdate(scope(), globalCfg(), projectCfg(), partial) }
|
||||
if (scope() === "global") {
|
||||
updateGlobalConfig(patch)
|
||||
return
|
||||
}
|
||||
updateProjectConfig(patch)
|
||||
}
|
||||
|
||||
const vectorStore = () => cfg().vectorStore ?? DEFAULT_VECTOR_STORE
|
||||
@@ -138,21 +177,6 @@ const IndexingTab: Component = () => {
|
||||
updateIndexing({ enabled })
|
||||
}
|
||||
|
||||
const saveGlobalEnabled = (enabled: boolean) => {
|
||||
if (enabled && !globalCfg().provider && !cfg().provider && kiloAvailable()) {
|
||||
updateGlobalConfig({
|
||||
indexing: {
|
||||
enabled,
|
||||
provider: "kilo",
|
||||
model: knownKiloModel(cfg().model) ?? (kiloDefault() || null),
|
||||
dimension: null,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
updateGlobalConfig({ indexing: { enabled } })
|
||||
}
|
||||
|
||||
const saveModel = (value: string) => {
|
||||
if (selectedProvider() === "kilo") return
|
||||
const trimmed = value.trim()
|
||||
@@ -160,7 +184,7 @@ const IndexingTab: Component = () => {
|
||||
}
|
||||
|
||||
const providerValue = (group: string, key: string) => {
|
||||
const draftKey = `${group}.${key}`
|
||||
const draftKey = `${scope()}.${group}.${key}`
|
||||
const draft = providerDrafts()[draftKey]
|
||||
if (draft !== undefined) return draft
|
||||
const value = (cfg()[group as keyof IndexingConfig] as Record<string, string | undefined> | undefined)?.[key]
|
||||
@@ -168,7 +192,7 @@ const IndexingTab: Component = () => {
|
||||
}
|
||||
|
||||
const storeValue = (group: "qdrant" | "lancedb", key: string) => {
|
||||
const draftKey = `${group}.${key}`
|
||||
const draftKey = `${scope()}.${group}.${key}`
|
||||
const draft = storeDrafts()[draftKey]
|
||||
if (draft !== undefined) return draft
|
||||
const value = (cfg()[group] as Record<string, string | undefined> | undefined)?.[key]
|
||||
@@ -176,13 +200,17 @@ const IndexingTab: Component = () => {
|
||||
}
|
||||
|
||||
const saveProviderField = (group: ProviderId, key: string, value: string) => {
|
||||
const current = (cfg()[group] as Record<string, string | undefined> | undefined) ?? {}
|
||||
const current = (raw()[group] as Record<string, string | undefined> | undefined) ?? {}
|
||||
updateIndexing({ [group]: { ...current, [key]: value.trim() || undefined } })
|
||||
const draftKey = `${scope()}.${group}.${key}`
|
||||
setProviderDrafts((prev) => Object.fromEntries(Object.entries(prev).filter(([entry]) => entry !== draftKey)))
|
||||
}
|
||||
|
||||
const saveStoreField = (group: "qdrant" | "lancedb", key: string, value: string) => {
|
||||
const current = (cfg()[group] as Record<string, string | undefined> | undefined) ?? {}
|
||||
const current = (raw()[group] as Record<string, string | undefined> | undefined) ?? {}
|
||||
updateIndexing({ [group]: { ...current, [key]: value.trim() || undefined } })
|
||||
const draftKey = `${scope()}.${group}.${key}`
|
||||
setStoreDrafts((prev) => Object.fromEntries(Object.entries(prev).filter(([entry]) => entry !== draftKey)))
|
||||
}
|
||||
|
||||
const saveNumber = (
|
||||
@@ -193,6 +221,10 @@ const IndexingTab: Component = () => {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) {
|
||||
updateIndexing({ [key]: key === "dimension" ? null : undefined })
|
||||
if (key !== "dimension") {
|
||||
const draftKey = `${scope()}.${key}`
|
||||
setTuningDrafts((prev) => Object.fromEntries(Object.entries(prev).filter(([entry]) => entry !== draftKey)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -202,10 +234,14 @@ const IndexingTab: Component = () => {
|
||||
if (options?.min !== undefined && num < options.min) return
|
||||
if (options?.max !== undefined && num > options.max) return
|
||||
updateIndexing({ [key]: num })
|
||||
if (key !== "dimension") {
|
||||
const draftKey = `${scope()}.${key}`
|
||||
setTuningDrafts((prev) => Object.fromEntries(Object.entries(prev).filter(([entry]) => entry !== draftKey)))
|
||||
}
|
||||
}
|
||||
|
||||
const tuningValue = (key: TuningKey) => {
|
||||
const draft = tuningDrafts()[key]
|
||||
const draft = tuningDrafts()[`${scope()}.${key}`]
|
||||
if (draft !== undefined) return draft
|
||||
const value = cfg()[key]
|
||||
return value === undefined ? "" : String(value)
|
||||
@@ -220,34 +256,55 @@ const IndexingTab: Component = () => {
|
||||
</span>
|
||||
</SettingsRow>
|
||||
<SettingsRow
|
||||
title={language.t("settings.indexing.globalEnable.title")}
|
||||
description={language.t("settings.indexing.globalEnable.description")}
|
||||
title="Configuration scope"
|
||||
description={
|
||||
scope() === "global"
|
||||
? language.t("settings.indexing.globalEnable.description")
|
||||
: language.t("settings.indexing.projectEnable.description")
|
||||
}
|
||||
>
|
||||
<Switch checked={globalCfg().enabled ?? false} onChange={saveGlobalEnabled} hideLabel>
|
||||
{language.t("settings.indexing.globalEnable.title")}
|
||||
</Switch>
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<Button
|
||||
variant={scope() === "global" ? "primary" : "secondary"}
|
||||
size="small"
|
||||
onClick={() => changeScope("global")}
|
||||
>
|
||||
{language.t("settings.config.scope.global")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={scope() === "project" ? "primary" : "secondary"}
|
||||
size="small"
|
||||
onClick={() => changeScope("project")}
|
||||
>
|
||||
{language.t("settings.config.scope.local")}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
<SettingsRow
|
||||
title={language.t("settings.indexing.projectEnable.title")}
|
||||
description={language.t("settings.indexing.projectEnable.description")}
|
||||
title={
|
||||
scope() === "global"
|
||||
? language.t("settings.indexing.globalEnable.title")
|
||||
: language.t("settings.indexing.projectEnable.title")
|
||||
}
|
||||
description={
|
||||
inherited()
|
||||
? `Inherited from global config (${enabled() ? "on" : "off"}) until a project value is saved.`
|
||||
: language.t("settings.indexing.enable.description")
|
||||
}
|
||||
tag={tag(scope(), [["enabled"]])}
|
||||
last
|
||||
>
|
||||
<Tooltip
|
||||
value={language.t("settings.indexing.projectEnable.disabledTooltip")}
|
||||
placement="top"
|
||||
inactive={!globalOn()}
|
||||
>
|
||||
<Switch checked={cfg().enabled === true} onChange={saveEnabled} disabled={globalOn()} hideLabel>
|
||||
{language.t("settings.indexing.projectEnable.title")}
|
||||
</Switch>
|
||||
</Tooltip>
|
||||
<Switch checked={enabled()} onChange={saveEnabled} hideLabel>
|
||||
{language.t("settings.indexing.enable.title")}
|
||||
</Switch>
|
||||
</SettingsRow>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<SettingsRow
|
||||
title={language.t("settings.indexing.provider.title")}
|
||||
description={language.t("settings.indexing.provider.description")}
|
||||
description={description(language.t("settings.indexing.provider.description"), [["provider"]])}
|
||||
tag={tag(scope(), [["provider"]])}
|
||||
>
|
||||
<Select
|
||||
options={providers()}
|
||||
@@ -265,7 +322,8 @@ const IndexingTab: Component = () => {
|
||||
<Show when={kiloModels().length > 0}>
|
||||
<SettingsRow
|
||||
title={language.t("settings.indexing.kiloModel.title")}
|
||||
description={language.t("settings.indexing.kiloModel.description")}
|
||||
description={description(language.t("settings.indexing.kiloModel.description"), [["model"]])}
|
||||
tag={tag(scope(), [["model"]])}
|
||||
>
|
||||
<Select
|
||||
options={kiloModels()}
|
||||
@@ -284,14 +342,20 @@ const IndexingTab: Component = () => {
|
||||
<Show when={selectedProvider() !== "kilo"}>
|
||||
<SettingsRow
|
||||
title={language.t("settings.indexing.model.title")}
|
||||
description={language.t("settings.indexing.model.description")}
|
||||
description={description(language.t("settings.indexing.model.description"), [["model"]])}
|
||||
tag={tag(scope(), [["model"]])}
|
||||
>
|
||||
<TextField value={cfg().model ?? ""} placeholder="Enter model ID" onChange={saveModel} />
|
||||
</SettingsRow>
|
||||
</Show>
|
||||
<SettingsRow
|
||||
title={language.t("settings.indexing.dimension.title")}
|
||||
description={language.t("settings.indexing.dimension.description")}
|
||||
description={
|
||||
selectedProvider() === "kilo"
|
||||
? language.t("settings.indexing.dimension.description")
|
||||
: description(language.t("settings.indexing.dimension.description"), [["dimension"]])
|
||||
}
|
||||
tag={selectedProvider() === "kilo" ? undefined : tag(scope(), [["dimension"]])}
|
||||
last={!selectedProvider() || (fields().length === 0 && !(selectedProvider() === "kilo" && !kiloAvailable()))}
|
||||
>
|
||||
<TextField
|
||||
@@ -300,7 +364,10 @@ const IndexingTab: Component = () => {
|
||||
? ""
|
||||
: String(cfg().dimension)
|
||||
}
|
||||
placeholder={language.t("settings.indexing.dimension.placeholder")}
|
||||
placeholder={
|
||||
selectedProvider() === "kilo" ? "Provided by Kilo" : language.t("settings.indexing.dimension.placeholder")
|
||||
}
|
||||
disabled={selectedProvider() === "kilo"}
|
||||
onChange={(value) => saveNumber("dimension", value, { integer: true, min: 1 })}
|
||||
/>
|
||||
</SettingsRow>
|
||||
@@ -316,13 +383,16 @@ const IndexingTab: Component = () => {
|
||||
<Show when={fields().length > 0 ? selectedProvider() : undefined} keyed>
|
||||
{(group) => {
|
||||
const fields = providerFields(group)
|
||||
const label = allProviders.find((item) => item.value === group)?.label ?? group
|
||||
const name = allProviders.find((item) => item.value === group)?.label ?? group
|
||||
return (
|
||||
<For each={fields}>
|
||||
{(field, index) => (
|
||||
<SettingsRow
|
||||
title={`${label} ${field.label}`}
|
||||
description={language.t("settings.indexing.providerField.description")}
|
||||
title={`${name} ${field.label}`}
|
||||
description={description(language.t("settings.indexing.providerField.description"), [
|
||||
[group, field.key],
|
||||
])}
|
||||
tag={tag(scope(), [[group, field.key]])}
|
||||
last={index() === fields.length - 1}
|
||||
>
|
||||
<TextField
|
||||
@@ -331,7 +401,7 @@ const IndexingTab: Component = () => {
|
||||
placeholder={field.placeholder}
|
||||
onInput={(e: InputEvent) => {
|
||||
const target = e.currentTarget as HTMLInputElement
|
||||
setProviderDrafts((prev) => ({ ...prev, [`${group}.${field.key}`]: target.value }))
|
||||
setProviderDrafts((prev) => ({ ...prev, [`${scope()}.${group}.${field.key}`]: target.value }))
|
||||
}}
|
||||
onBlur={(e: FocusEvent) => {
|
||||
const target = e.currentTarget as HTMLInputElement
|
||||
@@ -349,7 +419,8 @@ const IndexingTab: Component = () => {
|
||||
<Card>
|
||||
<SettingsRow
|
||||
title={language.t("settings.indexing.vectorStore.title")}
|
||||
description={language.t("settings.indexing.vectorStore.description")}
|
||||
description={description(language.t("settings.indexing.vectorStore.description"), [["vectorStore"]])}
|
||||
tag={tag(scope(), [["vectorStore"]])}
|
||||
>
|
||||
<Select
|
||||
options={stores}
|
||||
@@ -367,7 +438,10 @@ const IndexingTab: Component = () => {
|
||||
fallback={
|
||||
<SettingsRow
|
||||
title={language.t("settings.indexing.lancedbDirectory.title")}
|
||||
description={language.t("settings.indexing.lancedbDirectory.description")}
|
||||
description={description(language.t("settings.indexing.lancedbDirectory.description"), [
|
||||
["lancedb", "directory"],
|
||||
])}
|
||||
tag={tag(scope(), [["lancedb", "directory"]])}
|
||||
last
|
||||
>
|
||||
<TextField
|
||||
@@ -375,7 +449,7 @@ const IndexingTab: Component = () => {
|
||||
placeholder={language.t("settings.indexing.lancedbDirectory.placeholder")}
|
||||
onInput={(e: InputEvent) => {
|
||||
const target = e.currentTarget as HTMLInputElement
|
||||
setStoreDrafts((prev) => ({ ...prev, "lancedb.directory": target.value }))
|
||||
setStoreDrafts((prev) => ({ ...prev, [`${scope()}.lancedb.directory`]: target.value }))
|
||||
}}
|
||||
onBlur={(e: FocusEvent) => {
|
||||
const target = e.currentTarget as HTMLInputElement
|
||||
@@ -388,14 +462,15 @@ const IndexingTab: Component = () => {
|
||||
<>
|
||||
<SettingsRow
|
||||
title={language.t("settings.indexing.qdrantUrl.title")}
|
||||
description={language.t("settings.indexing.qdrantUrl.description")}
|
||||
description={description(language.t("settings.indexing.qdrantUrl.description"), [["qdrant", "url"]])}
|
||||
tag={tag(scope(), [["qdrant", "url"]])}
|
||||
>
|
||||
<TextField
|
||||
value={storeValue("qdrant", "url")}
|
||||
placeholder="http://localhost:6333"
|
||||
onInput={(e: InputEvent) => {
|
||||
const target = e.currentTarget as HTMLInputElement
|
||||
setStoreDrafts((prev) => ({ ...prev, "qdrant.url": target.value }))
|
||||
setStoreDrafts((prev) => ({ ...prev, [`${scope()}.qdrant.url`]: target.value }))
|
||||
}}
|
||||
onBlur={(e: FocusEvent) => {
|
||||
const target = e.currentTarget as HTMLInputElement
|
||||
@@ -405,7 +480,10 @@ const IndexingTab: Component = () => {
|
||||
</SettingsRow>
|
||||
<SettingsRow
|
||||
title={language.t("settings.indexing.qdrantApiKey.title")}
|
||||
description={language.t("settings.indexing.qdrantApiKey.description")}
|
||||
description={description(language.t("settings.indexing.qdrantApiKey.description"), [
|
||||
["qdrant", "apiKey"],
|
||||
])}
|
||||
tag={tag(scope(), [["qdrant", "apiKey"]])}
|
||||
last
|
||||
>
|
||||
<TextField
|
||||
@@ -414,7 +492,7 @@ const IndexingTab: Component = () => {
|
||||
placeholder={language.t("settings.indexing.qdrantApiKey.placeholder")}
|
||||
onInput={(e: InputEvent) => {
|
||||
const target = e.currentTarget as HTMLInputElement
|
||||
setStoreDrafts((prev) => ({ ...prev, "qdrant.apiKey": target.value }))
|
||||
setStoreDrafts((prev) => ({ ...prev, [`${scope()}.qdrant.apiKey`]: target.value }))
|
||||
}}
|
||||
onBlur={(e: FocusEvent) => {
|
||||
const target = e.currentTarget as HTMLInputElement
|
||||
@@ -431,7 +509,8 @@ const IndexingTab: Component = () => {
|
||||
{(item, index) => (
|
||||
<SettingsRow
|
||||
title={item.label}
|
||||
description={language.t("settings.indexing.tuning.description")}
|
||||
description={description(language.t("settings.indexing.tuning.description"), [[item.key]])}
|
||||
tag={tag(scope(), [[item.key]])}
|
||||
last={index() === tuning.length - 1}
|
||||
>
|
||||
<TextField
|
||||
@@ -439,7 +518,7 @@ const IndexingTab: Component = () => {
|
||||
placeholder={item.placeholder}
|
||||
onInput={(e: InputEvent) => {
|
||||
const target = e.currentTarget as HTMLInputElement
|
||||
setTuningDrafts((prev) => ({ ...prev, [item.key]: target.value }))
|
||||
setTuningDrafts((prev) => ({ ...prev, [`${scope()}.${item.key}`]: target.value }))
|
||||
}}
|
||||
onBlur={(e: FocusEvent) => {
|
||||
const target = e.currentTarget as HTMLInputElement
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Component, JSX } from "solid-js"
|
||||
import { Tag } from "@kilocode/kilo-ui/tag"
|
||||
import { Component, JSX, Show } from "solid-js"
|
||||
|
||||
const SettingsRow: Component<{ title: string; description?: string; last?: boolean; children: JSX.Element }> = (
|
||||
props,
|
||||
) => (
|
||||
const SettingsRow: Component<{
|
||||
title: string
|
||||
description?: string
|
||||
tag?: string
|
||||
last?: boolean
|
||||
children: JSX.Element
|
||||
}> = (props) => (
|
||||
<div
|
||||
data-slot="settings-row"
|
||||
style={{
|
||||
@@ -15,9 +20,16 @@ const SettingsRow: Component<{ title: string; description?: string; last?: boole
|
||||
<div data-slot="settings-row-label">
|
||||
<div
|
||||
data-slot="settings-row-label-title"
|
||||
style={props.description === null || props.description === undefined ? { "margin-bottom": "0" } : {}}
|
||||
style={{
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
gap: "6px",
|
||||
"flex-wrap": "wrap",
|
||||
...(props.description === null || props.description === undefined ? { "margin-bottom": "0" } : {}),
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
<span>{props.title}</span>
|
||||
<Show when={props.tag}>{(tag) => <Tag>{tag()}</Tag>}</Show>
|
||||
</div>
|
||||
{props.description !== null && props.description !== undefined && (
|
||||
<div data-slot="settings-row-label-subtitle">{props.description}</div>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { IndexingConfig } from "@kilocode/kilo-indexing/config"
|
||||
|
||||
export type IndexingScope = "global" | "project"
|
||||
export type IndexingInheritance = "none" | "inherited" | "partial"
|
||||
export type IndexingSource = "none" | "global" | "local" | "mixed" | "default"
|
||||
|
||||
function record(input: unknown): input is Record<string, unknown> {
|
||||
return typeof input === "object" && input !== null && !Array.isArray(input)
|
||||
}
|
||||
|
||||
function mergeEffective(base: Record<string, unknown>, patch: Record<string, unknown>) {
|
||||
const result: Record<string, unknown> = { ...base }
|
||||
for (const [key, value] of Object.entries(patch)) {
|
||||
if (value === undefined) continue
|
||||
if (record(value) && record(result[key])) {
|
||||
result[key] = mergeEffective(result[key], value)
|
||||
continue
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function mergeUpdate(base: Record<string, unknown>, patch: Record<string, unknown>) {
|
||||
const result: Record<string, unknown> = { ...base }
|
||||
for (const [key, value] of Object.entries(patch)) {
|
||||
if (record(value) && record(result[key])) {
|
||||
result[key] = mergeUpdate(result[key], value)
|
||||
continue
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function get(input: IndexingConfig, path: readonly string[]) {
|
||||
return path.reduce<unknown>((value, key) => (record(value) ? value[key] : undefined), input)
|
||||
}
|
||||
|
||||
export function indexingConfig(scope: IndexingScope, global: IndexingConfig, project: IndexingConfig) {
|
||||
if (scope === "global") return global
|
||||
return mergeEffective(global, project) as IndexingConfig
|
||||
}
|
||||
|
||||
export function indexingUpdate(
|
||||
scope: IndexingScope,
|
||||
global: IndexingConfig,
|
||||
project: IndexingConfig,
|
||||
patch: IndexingConfig,
|
||||
) {
|
||||
return mergeUpdate(scope === "global" ? global : project, patch) as IndexingConfig
|
||||
}
|
||||
|
||||
export function indexingSource(
|
||||
scope: IndexingScope,
|
||||
global: IndexingConfig,
|
||||
project: IndexingConfig,
|
||||
paths: readonly (readonly string[])[],
|
||||
): IndexingSource {
|
||||
if (scope !== "project") return "none"
|
||||
const local = paths.filter((path) => get(project, path) !== undefined).length
|
||||
const inherited = paths.filter((path) => get(project, path) === undefined && get(global, path) !== undefined).length
|
||||
if (local > 0 && inherited > 0) return "mixed"
|
||||
if (local > 0) return "local"
|
||||
if (inherited > 0) return "global"
|
||||
return "default"
|
||||
}
|
||||
|
||||
export function indexingInheritance(
|
||||
scope: IndexingScope,
|
||||
global: IndexingConfig,
|
||||
project: IndexingConfig,
|
||||
paths: readonly (readonly string[])[],
|
||||
): IndexingInheritance {
|
||||
const source = indexingSource(scope, global, project, paths)
|
||||
if (source === "global") return "inherited"
|
||||
if (source === "mixed") return "partial"
|
||||
return "none"
|
||||
}
|
||||
|
||||
export function indexingDescription(description: string, inheritance: IndexingInheritance) {
|
||||
if (inheritance === "inherited") return `${description} Inherited from global config.`
|
||||
if (inheritance === "partial") return `${description} Some values are inherited from global config.`
|
||||
return description
|
||||
}
|
||||
|
||||
export function indexingEnabled(scope: IndexingScope, global: IndexingConfig, project: IndexingConfig) {
|
||||
return indexingConfig(scope, global, project).enabled === true
|
||||
}
|
||||
|
||||
export function indexingEnabledInherited(scope: IndexingScope, global: IndexingConfig, project: IndexingConfig) {
|
||||
return indexingInheritance(scope, global, project, [["enabled"]]) === "inherited"
|
||||
}
|
||||
@@ -12,7 +12,14 @@ import { createContext, useContext, createSignal, createMemo, onCleanup } from "
|
||||
import type { ParentComponent, Accessor } from "solid-js"
|
||||
import { useVSCode } from "./vscode"
|
||||
import type { Config, ExtensionMessage, FeatureFlags } from "../types/messages"
|
||||
import { deepMerge, stripNulls, resolveConfig } from "../utils/config-utils"
|
||||
import {
|
||||
configUnsetPaths,
|
||||
deepMerge,
|
||||
mergeScopedConfig,
|
||||
pruneConfigSet,
|
||||
stripNulls,
|
||||
resolveConfig,
|
||||
} from "../utils/config-utils"
|
||||
import { splitConfigByScope } from "../utils/config-scope"
|
||||
|
||||
function has(value: Record<string, unknown>) {
|
||||
@@ -27,6 +34,7 @@ export interface SaveError {
|
||||
interface ConfigContextValue {
|
||||
config: Accessor<Config>
|
||||
globalConfig: Accessor<Config>
|
||||
projectConfig: Accessor<Config>
|
||||
settings: Accessor<Record<string, unknown>>
|
||||
features: Accessor<FeatureFlags>
|
||||
loading: Accessor<boolean>
|
||||
@@ -35,6 +43,7 @@ interface ConfigContextValue {
|
||||
saveError: Accessor<SaveError | null>
|
||||
updateConfig: (partial: Partial<Config>) => void
|
||||
updateGlobalConfig: (partial: Partial<Config>) => void
|
||||
updateProjectConfig: (partial: Partial<Config>) => void
|
||||
updateSetting: (key: string, value: unknown) => void
|
||||
saveConfig: () => void
|
||||
discardConfig: () => void
|
||||
@@ -47,19 +56,25 @@ export const ConfigProvider: ParentComponent = (props) => {
|
||||
|
||||
const [config, setConfig] = createSignal<Config>({})
|
||||
const [globalConfig, setGlobalConfig] = createSignal<Config>({})
|
||||
const [projectConfig, setProjectConfig] = createSignal<Config>({})
|
||||
const [settings, setSettings] = createSignal<Record<string, unknown>>({})
|
||||
const [features, setFeatures] = createSignal<FeatureFlags>({ indexing: false })
|
||||
const [loading, setLoading] = createSignal(true)
|
||||
const [draft, setDraft] = createSignal<Partial<Config>>({})
|
||||
const [globalDraft, setGlobalDraft] = createSignal<Partial<Config>>({})
|
||||
const [projectDraft, setProjectDraft] = createSignal<Partial<Config>>({})
|
||||
const [settingsDraft, setSettingsDraft] = createSignal<Record<string, unknown>>({})
|
||||
const isDirty = createMemo(
|
||||
() =>
|
||||
has(draft() as Record<string, unknown>) || has(globalDraft() as Record<string, unknown>) || has(settingsDraft()),
|
||||
has(draft() as Record<string, unknown>) ||
|
||||
has(globalDraft() as Record<string, unknown>) ||
|
||||
has(projectDraft() as Record<string, unknown>) ||
|
||||
has(settingsDraft()),
|
||||
)
|
||||
// Last config received from the server — used to revert on discard
|
||||
const [saved, setSaved] = createSignal<Config>({})
|
||||
const [savedGlobal, setSavedGlobal] = createSignal<Config>({})
|
||||
const [savedProject, setSavedProject] = createSignal<Config>({})
|
||||
const [savedSettings, setSavedSettings] = createSignal<Record<string, unknown>>({})
|
||||
// True while a saveConfig() write is in-flight — used to clear draft on success
|
||||
// and to guard against stale configLoaded messages overwriting optimistic state.
|
||||
@@ -91,15 +106,19 @@ export const ConfigProvider: ParentComponent = (props) => {
|
||||
setFeatures(message.features)
|
||||
setSaved(message.config)
|
||||
if (message.globalConfig !== undefined) {
|
||||
setGlobalConfig(stripNulls(deepMerge(message.globalConfig, globalDraft())))
|
||||
setGlobalConfig(mergeScopedConfig(message.globalConfig, globalDraft()))
|
||||
setSavedGlobal(message.globalConfig)
|
||||
}
|
||||
if (message.projectConfig !== undefined) {
|
||||
setProjectConfig(mergeScopedConfig(message.projectConfig, projectDraft()))
|
||||
setSavedProject(message.projectConfig)
|
||||
}
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
if (message.type === "globalConfigLoaded") {
|
||||
if (saving()) return
|
||||
setGlobalConfig(stripNulls(deepMerge(message.config, globalDraft())))
|
||||
setGlobalConfig(mergeScopedConfig(message.config, globalDraft()))
|
||||
setSavedGlobal(message.config)
|
||||
return
|
||||
}
|
||||
@@ -110,21 +129,30 @@ export const ConfigProvider: ParentComponent = (props) => {
|
||||
setSaving(false)
|
||||
setDraft({})
|
||||
setGlobalDraft({})
|
||||
setProjectDraft({})
|
||||
setSaveError(null)
|
||||
setConfig(message.config)
|
||||
if (message.globalConfig !== undefined) {
|
||||
setGlobalConfig(stripNulls(deepMerge(message.globalConfig, globalDraft())))
|
||||
setGlobalConfig(mergeScopedConfig(message.globalConfig, globalDraft()))
|
||||
setSavedGlobal(message.globalConfig)
|
||||
}
|
||||
if (message.projectConfig !== undefined) {
|
||||
setProjectConfig(message.projectConfig)
|
||||
setSavedProject(message.projectConfig)
|
||||
}
|
||||
setFeatures(message.features)
|
||||
} else {
|
||||
// configUpdated from a different source (e.g. PermissionDock save).
|
||||
// Re-apply the draft on top so pending settings changes are preserved.
|
||||
setConfig(resolveConfig(message.config, draft(), has(draft() as Record<string, unknown>)))
|
||||
if (message.globalConfig !== undefined) {
|
||||
setGlobalConfig(stripNulls(deepMerge(message.globalConfig, globalDraft())))
|
||||
setGlobalConfig(mergeScopedConfig(message.globalConfig, globalDraft()))
|
||||
setSavedGlobal(message.globalConfig)
|
||||
}
|
||||
if (message.projectConfig !== undefined) {
|
||||
setProjectConfig(mergeScopedConfig(message.projectConfig, projectDraft()))
|
||||
setSavedProject(message.projectConfig)
|
||||
}
|
||||
setFeatures(message.features)
|
||||
}
|
||||
setSaved(message.config)
|
||||
@@ -186,11 +214,17 @@ export const ConfigProvider: ParentComponent = (props) => {
|
||||
}
|
||||
|
||||
function updateGlobalConfig(partial: Partial<Config>) {
|
||||
setGlobalConfig((prev) => stripNulls(deepMerge(prev, partial)))
|
||||
setGlobalConfig((prev) => mergeScopedConfig(prev, partial))
|
||||
setGlobalDraft((prev) => deepMerge(prev as Config, partial))
|
||||
setSaveError(null)
|
||||
}
|
||||
|
||||
function updateProjectConfig(partial: Partial<Config>) {
|
||||
setProjectConfig((prev) => mergeScopedConfig(prev, partial))
|
||||
setProjectDraft((prev) => deepMerge(prev as Config, partial))
|
||||
setSaveError(null)
|
||||
}
|
||||
|
||||
function updateSetting(key: string, value: unknown) {
|
||||
setSettings((prev) => ({ ...prev, [key]: value }))
|
||||
setSettingsDraft((prev) => ({ ...prev, [key]: value }))
|
||||
@@ -200,11 +234,13 @@ export const ConfigProvider: ParentComponent = (props) => {
|
||||
function saveConfig() {
|
||||
const changes = draft()
|
||||
const globals = globalDraft()
|
||||
const projects = projectDraft()
|
||||
const pending = settingsDraft()
|
||||
const configDirty = has(changes as Record<string, unknown>)
|
||||
const globalDirty = has(globals as Record<string, unknown>)
|
||||
const projectDirty = has(projects as Record<string, unknown>)
|
||||
const settingsDirty = has(pending)
|
||||
if (!configDirty && !globalDirty && !settingsDirty) return
|
||||
if (!configDirty && !globalDirty && !projectDirty && !settingsDirty) return
|
||||
// Don't clear draft/isDirty yet — wait for configUpdated confirmation.
|
||||
// If the write fails, the save bar stays visible so the user can retry.
|
||||
setSaving(true)
|
||||
@@ -216,7 +252,7 @@ export const ConfigProvider: ParentComponent = (props) => {
|
||||
setSavedSettings((prev) => ({ ...prev, ...pending }))
|
||||
setSettingsDraft({})
|
||||
}
|
||||
if (!configDirty && !globalDirty) {
|
||||
if (!configDirty && !globalDirty && !projectDirty) {
|
||||
setSaving(false)
|
||||
return
|
||||
}
|
||||
@@ -225,14 +261,23 @@ export const ConfigProvider: ParentComponent = (props) => {
|
||||
// extension confirms only after both scopes are saved.
|
||||
const split = splitConfigByScope(changes)
|
||||
const next = deepMerge(split.global as Config, globals)
|
||||
vscode.postMessage({ type: "updateConfig", config: next, projectConfig: split.project })
|
||||
const project = deepMerge(split.project as Config, projects)
|
||||
vscode.postMessage({
|
||||
type: "updateConfig",
|
||||
config: pruneConfigSet(next) as Config,
|
||||
projectConfig: pruneConfigSet(project) as Config,
|
||||
globalUnset: configUnsetPaths(next),
|
||||
projectUnset: configUnsetPaths(project),
|
||||
})
|
||||
}
|
||||
|
||||
function discardConfig() {
|
||||
setConfig(saved())
|
||||
setGlobalConfig(savedGlobal())
|
||||
setProjectConfig(savedProject())
|
||||
setDraft({})
|
||||
setGlobalDraft({})
|
||||
setProjectDraft({})
|
||||
setSettings(savedSettings())
|
||||
setSettingsDraft({})
|
||||
setSaveError(null)
|
||||
@@ -241,6 +286,7 @@ export const ConfigProvider: ParentComponent = (props) => {
|
||||
const value: ConfigContextValue = {
|
||||
config,
|
||||
globalConfig,
|
||||
projectConfig,
|
||||
settings,
|
||||
features,
|
||||
loading,
|
||||
@@ -249,6 +295,7 @@ export const ConfigProvider: ParentComponent = (props) => {
|
||||
saveError,
|
||||
updateConfig,
|
||||
updateGlobalConfig,
|
||||
updateProjectConfig,
|
||||
updateSetting,
|
||||
saveConfig,
|
||||
discardConfig,
|
||||
|
||||
@@ -274,16 +274,30 @@ interface StoryProvidersProps {
|
||||
sessionID?: string
|
||||
/** When provided, injects a mock ConfigContext with this config instead of the real ConfigProvider. */
|
||||
config?: Config
|
||||
globalConfig?: Config
|
||||
projectConfig?: Config
|
||||
onConfigChange?: (config: Config) => void
|
||||
onGlobalConfigChange?: (config: Config) => void
|
||||
onProjectConfigChange?: (config: Config) => void
|
||||
kiloAuth?: boolean
|
||||
/** When true, renders children without the default 12px padding wrapper */
|
||||
noPadding?: boolean
|
||||
}
|
||||
|
||||
/** Wraps children with either a mock ConfigContext (when config prop is given) or the real ConfigProvider. */
|
||||
const ConfigWrapper: ParentComponent<{ config?: Config; onConfigChange?: (config: Config) => void }> = (props) => {
|
||||
const ConfigWrapper: ParentComponent<{
|
||||
config?: Config
|
||||
globalConfig?: Config
|
||||
projectConfig?: Config
|
||||
onConfigChange?: (config: Config) => void
|
||||
onGlobalConfigChange?: (config: Config) => void
|
||||
onProjectConfigChange?: (config: Config) => void
|
||||
}> = (props) => {
|
||||
if (props.config) {
|
||||
const scoped = props.globalConfig !== undefined || props.projectConfig !== undefined
|
||||
const [cfg, setCfg] = createSignal(props.config)
|
||||
const [global, setGlobal] = createSignal(props.globalConfig ?? props.config)
|
||||
const [project, setProject] = createSignal(props.projectConfig ?? props.config)
|
||||
const [settings, setSettings] = createSignal<Record<string, unknown>>({})
|
||||
const [dirty, setDirty] = createSignal(false)
|
||||
const features = createMemo(() => {
|
||||
@@ -298,7 +312,8 @@ const ConfigWrapper: ParentComponent<{ config?: Config; onConfigChange?: (config
|
||||
|
||||
const value = {
|
||||
config: createMemo(() => cfg()),
|
||||
globalConfig: createMemo(() => cfg()),
|
||||
globalConfig: createMemo(() => (scoped ? global() : cfg())),
|
||||
projectConfig: createMemo(() => (scoped ? project() : cfg())),
|
||||
settings,
|
||||
features,
|
||||
loading: () => false,
|
||||
@@ -314,11 +329,25 @@ const ConfigWrapper: ParentComponent<{ config?: Config; onConfigChange?: (config
|
||||
setDirty(true)
|
||||
},
|
||||
updateGlobalConfig: (partial: Partial<Config>) => {
|
||||
setCfg((prev) => {
|
||||
const update = (prev: Config) => {
|
||||
const next = merge(prev as Record<string, unknown>, partial as Record<string, unknown>) as Config
|
||||
props.onGlobalConfigChange?.(next)
|
||||
props.onConfigChange?.(next)
|
||||
return next
|
||||
})
|
||||
}
|
||||
if (scoped) setGlobal(update)
|
||||
if (!scoped) setCfg(update)
|
||||
setDirty(true)
|
||||
},
|
||||
updateProjectConfig: (partial: Partial<Config>) => {
|
||||
const update = (prev: Config) => {
|
||||
const next = merge(prev as Record<string, unknown>, partial as Record<string, unknown>) as Config
|
||||
props.onProjectConfigChange?.(next)
|
||||
props.onConfigChange?.(next)
|
||||
return next
|
||||
}
|
||||
if (scoped) setProject(update)
|
||||
if (!scoped) setCfg(update)
|
||||
setDirty(true)
|
||||
},
|
||||
updateSetting: (key: string, value: unknown) => {
|
||||
@@ -349,7 +378,14 @@ export const StoryProviders: ParentComponent<StoryProvidersProps> = (props) => {
|
||||
<VSCodeProvider>
|
||||
<ServerProvider>
|
||||
<FeedbackProvider>
|
||||
<ConfigWrapper config={props.config} onConfigChange={props.onConfigChange}>
|
||||
<ConfigWrapper
|
||||
config={props.config}
|
||||
globalConfig={props.globalConfig}
|
||||
projectConfig={props.projectConfig}
|
||||
onConfigChange={props.onConfigChange}
|
||||
onGlobalConfigChange={props.onGlobalConfigChange}
|
||||
onProjectConfigChange={props.onProjectConfigChange}
|
||||
>
|
||||
<DisplayProvider>
|
||||
<MockProviderProvider kiloAuth={props.kiloAuth}>
|
||||
<DialogProvider>
|
||||
|
||||
@@ -443,6 +443,49 @@ export const IndexingProviderBlurRace: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
export const IndexingScopeSwitch: Story = {
|
||||
name: "IndexingTab - global and local scopes",
|
||||
render: () => {
|
||||
const [global, setGlobal] = createSignal<Record<string, unknown>>({})
|
||||
const [project, setProject] = createSignal<Record<string, unknown>>({})
|
||||
const globalConfig: Config = {
|
||||
indexing: {
|
||||
enabled: true,
|
||||
provider: "openai",
|
||||
model: "text-embedding-3-large",
|
||||
dimension: 3072,
|
||||
vectorStore: "qdrant",
|
||||
openai: { apiKey: "global-secret" },
|
||||
qdrant: { url: "http://global:6333", apiKey: "global-qdrant" },
|
||||
searchMinScore: 0.4,
|
||||
},
|
||||
}
|
||||
const projectConfig: Config = {
|
||||
indexing: {
|
||||
model: null,
|
||||
qdrant: { apiKey: "project-qdrant" },
|
||||
},
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<StoryProviders
|
||||
config={globalConfig}
|
||||
globalConfig={globalConfig}
|
||||
projectConfig={projectConfig}
|
||||
onGlobalConfigChange={(next) => setGlobal((next.indexing ?? {}) as Record<string, unknown>)}
|
||||
onProjectConfigChange={(next) => setProject((next.indexing ?? {}) as Record<string, unknown>)}
|
||||
>
|
||||
<div style={{ width: "420px", "max-height": "700px", overflow: "auto" }}>
|
||||
<IndexingTab />
|
||||
</div>
|
||||
</StoryProviders>
|
||||
<pre data-testid="indexing-global-save">{JSON.stringify(global(), null, 2)}</pre>
|
||||
<pre data-testid="indexing-project-save">{JSON.stringify(project(), null, 2)}</pre>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const IndexingKiloModelPreset: Story = {
|
||||
name: "IndexingTab - Kilo stale custom model fallback",
|
||||
render: () => {
|
||||
|
||||
@@ -462,6 +462,7 @@ export interface ConfigLoadedMessage {
|
||||
type: "configLoaded"
|
||||
config: Config
|
||||
globalConfig?: Config
|
||||
projectConfig?: Config
|
||||
features: FeatureFlags
|
||||
}
|
||||
|
||||
@@ -469,6 +470,7 @@ export interface ConfigUpdatedMessage {
|
||||
type: "configUpdated"
|
||||
config: Config
|
||||
globalConfig?: Config
|
||||
projectConfig?: Config
|
||||
features: FeatureFlags
|
||||
}
|
||||
|
||||
|
||||
@@ -425,8 +425,10 @@ export interface UpdateConfigMessage {
|
||||
type: "updateConfig"
|
||||
/** Global config patch written to ~/.config/kilo/kilo.json. */
|
||||
config: Partial<Config>
|
||||
/** Project config patch written to the workspace's .kilo/kilo.json or existing project config. */
|
||||
globalUnset?: string[][]
|
||||
/** Project config patch written to the workspace's .kilo/kilo.jsonc or existing project config. */
|
||||
projectConfig?: Partial<Config>
|
||||
projectUnset?: string[][]
|
||||
}
|
||||
|
||||
export interface RequestNotificationSettingsMessage {
|
||||
|
||||
@@ -17,6 +17,51 @@ export function deepMerge(target: Config, source: Partial<Config>): Config {
|
||||
return result as Config
|
||||
}
|
||||
|
||||
function stripUndefined(value: unknown): unknown {
|
||||
if (!isRecord(value)) return value
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).flatMap(([key, item]) => {
|
||||
if (item === undefined) return []
|
||||
return [[key, stripUndefined(item)]]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/** Merge raw scoped config while preserving schema-valid indexing null overrides. */
|
||||
export function mergeScopedConfig(target: Config, source: Partial<Config>): Config {
|
||||
const merged = deepMerge(target, source)
|
||||
const result = stripNulls(merged)
|
||||
if (isRecord(merged.indexing)) result.indexing = stripUndefined(merged.indexing) as Config["indexing"]
|
||||
return result
|
||||
}
|
||||
|
||||
function indexingNull(path: readonly string[]) {
|
||||
return path.length === 2 && path[0] === "indexing" && (path[1] === "model" || path[1] === "dimension")
|
||||
}
|
||||
|
||||
export function configUnsetPaths(value: unknown, prefix: string[] = []): string[][] {
|
||||
if (!isRecord(value)) return []
|
||||
return Object.entries(value).flatMap(([key, item]) => {
|
||||
const path = [...prefix, key]
|
||||
if (item === undefined || (item === null && !indexingNull(path))) return [path]
|
||||
return configUnsetPaths(item, path)
|
||||
})
|
||||
}
|
||||
|
||||
/** Prepare an overlay set payload while preserving schema-valid indexing null overrides. */
|
||||
export function pruneConfigSet(value: unknown, prefix: string[] = []): unknown {
|
||||
if (!isRecord(value)) return value
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).flatMap(([key, item]) => {
|
||||
const path = [...prefix, key]
|
||||
if (item === undefined || (item === null && !indexingNull(path))) return []
|
||||
const next = pruneConfigSet(item, path)
|
||||
if (path[0] === "indexing" && isRecord(next) && Object.keys(next).length === 0) return []
|
||||
return [[key, next]]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/** Recursively remove keys whose value is null (null = "deleted"). */
|
||||
export function stripNulls(obj: Config): Config {
|
||||
const result: Record<string, unknown> = {}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { Show, splitProps, type ComponentProps, type JSX } from "solid-js"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "./card"
|
||||
import { Icon, type IconProps } from "./icon"
|
||||
import { Tag, CountTag } from "./tag"
|
||||
import { Tag } from "./tag"
|
||||
|
||||
export { CountTag }
|
||||
export function ConfigTag(props: ComponentProps<typeof Tag>) {
|
||||
return <Tag {...props} data-slot="config-tag" />
|
||||
}
|
||||
|
||||
export function ConfigCountTag(props: ComponentProps<"span">) {
|
||||
return <ConfigTag {...props} tone="neutral" />
|
||||
}
|
||||
|
||||
export { ConfigCountTag as CountTag }
|
||||
|
||||
type Tone = "neutral" | "success" | "warning" | "critical" | "info" | "brand"
|
||||
type Source = "default" | "global" | "project" | "system" | "inherited" | "local override" | string | undefined
|
||||
@@ -39,10 +47,10 @@ export function StatusTag(props: {
|
||||
return { tone: "neutral" as const, label: props.status || "Unknown" }
|
||||
}
|
||||
return (
|
||||
<Tag tone={meta().tone} class="kw-status-tag">
|
||||
<ConfigTag tone={meta().tone} class="kw-status-tag">
|
||||
<StatusDot tone={meta().tone === "critical" ? "critical" : meta().tone === "success" ? "success" : "neutral"} />
|
||||
{meta().label}
|
||||
</Tag>
|
||||
</ConfigTag>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,9 +58,9 @@ export function SourceBadge(props: { source?: Source; inherited?: boolean; overr
|
||||
const source = () => kind(props)
|
||||
const label = () => source().toUpperCase()
|
||||
return (
|
||||
<Tag class="mono" tone={source() === "project" ? "info" : "neutral"}>
|
||||
<ConfigTag class="mono" tone={source() === "project" ? "info" : "neutral"}>
|
||||
{label()}
|
||||
</Tag>
|
||||
</ConfigTag>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { isKiloError, showKiloErrorToast } from "@/kilocode/kilo-errors"
|
||||
import { registerKiloCommands } from "@/kilocode/kilo-commands"
|
||||
import { initializeTUIDependencies } from "@kilocode/kilo-gateway/tui"
|
||||
import { DialogProcessList } from "@/kilocode/cli/cmd/tui/component/dialog-process-list"
|
||||
import { useIndexingWarnings } from "@/kilocode/cli/cmd/tui/indexing-warning"
|
||||
|
||||
// Re-export so upstream can render the route without importing directly
|
||||
export { KiloClawView } from "@/kilocode/claw/view"
|
||||
@@ -155,6 +156,8 @@ export function init() {
|
||||
const toast = useToast()
|
||||
const dialog = useDialog()
|
||||
|
||||
useIndexingWarnings()
|
||||
|
||||
// Inject TUI dependencies for kilo-gateway
|
||||
initializeTUIDependencies({
|
||||
useCommandPalette,
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { createEffect, onCleanup } from "solid-js"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { useProject } from "@tui/context/project"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { useToast } from "@tui/ui/toast"
|
||||
import { Event as IndexingStatusEvent, Warning as IndexingWarningEvent } from "@/kilocode/indexing-event"
|
||||
import { indexingErrorMessage, indexingWarningKey, type IndexingWarning } from "@/kilocode/indexing-warning"
|
||||
|
||||
const log = Log.create({ service: "indexing-warning" })
|
||||
|
||||
export function useIndexingWarnings() {
|
||||
const sdk = useSDK()
|
||||
const toast = useToast()
|
||||
const project = useProject()
|
||||
const seen = new Set<string>()
|
||||
const state = { scope: "" }
|
||||
const show = (warning: IndexingWarning) => {
|
||||
const key = indexingWarningKey(warning)
|
||||
if (seen.has(key)) return
|
||||
seen.add(key)
|
||||
toast.show({
|
||||
title: "Qdrant Compatibility Warning",
|
||||
message: warning.message,
|
||||
variant: "warning",
|
||||
duration: 10000,
|
||||
})
|
||||
}
|
||||
const showError = (message: string) => {
|
||||
const key = `error\u0000${message}`
|
||||
if (seen.has(key)) return
|
||||
seen.add(key)
|
||||
toast.show({
|
||||
title: "Code Indexing Error",
|
||||
message,
|
||||
variant: "error",
|
||||
duration: 10000,
|
||||
})
|
||||
}
|
||||
|
||||
onCleanup(
|
||||
sdk.event.on("event", (event) => {
|
||||
if (event.payload.type !== IndexingWarningEvent.type && event.payload.type !== IndexingStatusEvent.type) return
|
||||
if (event.workspace !== project.workspace.current()) return
|
||||
const directory = project.instance.directory() || sdk.directory
|
||||
if (directory && event.directory !== directory) return
|
||||
if (event.payload.type === IndexingWarningEvent.type) {
|
||||
show(event.payload.properties)
|
||||
return
|
||||
}
|
||||
const message = indexingErrorMessage(event.payload.properties.status)
|
||||
if (message) showError(message)
|
||||
}),
|
||||
)
|
||||
createEffect(() => {
|
||||
const workspace = project.workspace.current()
|
||||
const directory = project.instance.directory() || sdk.directory || ""
|
||||
const scope = `${workspace ?? ""}\u0000${directory}`
|
||||
if (state.scope !== scope) {
|
||||
state.scope = scope
|
||||
seen.clear()
|
||||
}
|
||||
void Promise.all([
|
||||
sdk.client.indexing.warnings({ workspace }, { throwOnError: true }),
|
||||
sdk.client.indexing.status({ workspace }, { throwOnError: true }),
|
||||
])
|
||||
.then(([warnings, status]) => {
|
||||
if (project.workspace.current() !== workspace) return
|
||||
if ((project.instance.directory() || sdk.directory || "") !== directory) return
|
||||
for (const warning of warnings.data ?? []) show(warning)
|
||||
const message = status.data ? indexingErrorMessage(status.data) : undefined
|
||||
if (message) showError(message)
|
||||
})
|
||||
.catch((err) => log.debug("indexing notification replay failed", { err }))
|
||||
})
|
||||
}
|
||||
@@ -10,14 +10,25 @@ import { useDialog } from "@tui/ui/dialog"
|
||||
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
|
||||
import { DialogPrompt } from "@tui/ui/dialog-prompt"
|
||||
import { DEFAULT_VECTOR_STORE } from "@kilocode/kilo-indexing/config"
|
||||
import { formatKiloEmbeddingModelLabel } from "@kilocode/kilo-indexing/embedding-models"
|
||||
import { fetchKiloEmbeddingModelCatalog } from "@kilocode/kilo-gateway"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { useToast } from "@tui/ui/toast"
|
||||
import { createResource, Show } from "solid-js"
|
||||
import { createEffect, createMemo, createResource, createSignal, Show } from "solid-js"
|
||||
import { reconcile } from "solid-js/store"
|
||||
import type { IndexingConfig, Config } from "@kilocode/sdk/v2"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { hasKiloIndexingAuth, resolveKiloIndexingAuth, shouldDefaultIndexingToKilo } from "../indexing-auth"
|
||||
import {
|
||||
createIndexingDialogState,
|
||||
currentKiloModel,
|
||||
indexingInheritance,
|
||||
indexingPatch,
|
||||
indexingScopeConfig,
|
||||
inheritedDescription,
|
||||
kiloModelOptions,
|
||||
loadKiloEmbeddingModels,
|
||||
mergeIndexingConfig,
|
||||
type IndexingScope,
|
||||
} from "./indexing-dialog-state"
|
||||
|
||||
// These types are OpenCode-internal and imported at runtime
|
||||
type UseSDK = any
|
||||
@@ -25,6 +36,8 @@ type SDK = any
|
||||
|
||||
type EmbeddingProvider = NonNullable<IndexingConfig["provider"]>
|
||||
|
||||
const log = Log.create({ service: "indexing-model-select" })
|
||||
|
||||
const PROVIDER_LABELS: Record<EmbeddingProvider, string> = {
|
||||
kilo: "Kilo",
|
||||
openai: "OpenAI",
|
||||
@@ -73,93 +86,58 @@ function maskSecret(value: string | undefined): string {
|
||||
return value.slice(0, 3) + "..." + value.slice(-3)
|
||||
}
|
||||
|
||||
function getIndexing(sync: ReturnType<typeof useSync>): IndexingConfig {
|
||||
return (sync.data.config as Config & { indexing?: IndexingConfig }).indexing ?? {}
|
||||
}
|
||||
|
||||
function globalIndexing(data: Config | undefined): IndexingConfig {
|
||||
function scopedIndexing(data: Config | undefined): IndexingConfig {
|
||||
return data?.indexing ?? {}
|
||||
}
|
||||
|
||||
function hasKiloAuth(sync: ReturnType<typeof useSync>): boolean {
|
||||
function hasKiloAuth(sync: ReturnType<typeof useSync>, scope: IndexingScope, indexing: IndexingConfig): boolean {
|
||||
const provider = sync.data.provider_next.all.find((item) => item.id === "kilo")
|
||||
return hasKiloIndexingAuth({ config: sync.data.config, provider })
|
||||
const config = indexingScopeConfig(scope, sync.data.config, sync.data.globalConfig, indexing)
|
||||
return hasKiloIndexingAuth({ config, provider })
|
||||
}
|
||||
|
||||
function defaultIndexing(sync: ReturnType<typeof useSync>, global?: IndexingConfig): IndexingConfig {
|
||||
const indexing = getIndexing(sync)
|
||||
function defaultIndexing(
|
||||
sync: ReturnType<typeof useSync>,
|
||||
scope: IndexingScope,
|
||||
indexing: IndexingConfig,
|
||||
global?: IndexingConfig,
|
||||
): IndexingConfig {
|
||||
const provider = sync.data.provider_next.all.find((item) => item.id === "kilo")
|
||||
const auth = resolveKiloIndexingAuth({ config: sync.data.config, provider })
|
||||
const config = indexingScopeConfig(scope, sync.data.config, sync.data.globalConfig, indexing)
|
||||
const auth = resolveKiloIndexingAuth({ config, provider })
|
||||
if (!shouldDefaultIndexingToKilo({ ...global, ...indexing }, auth)) return indexing
|
||||
return { ...indexing, provider: "kilo", model: null, dimension: null }
|
||||
}
|
||||
|
||||
async function saveIndexing(
|
||||
async function saveScopedIndexing(
|
||||
sdk: SDK,
|
||||
sync: ReturnType<typeof useSync>,
|
||||
scope: IndexingScope,
|
||||
before: IndexingConfig,
|
||||
indexing: IndexingConfig,
|
||||
toast: ReturnType<typeof useToast>,
|
||||
): Promise<boolean> {
|
||||
const global = { ...indexing }
|
||||
delete global.enabled
|
||||
const responses = await Promise.all([
|
||||
...(Object.keys(global).length > 0 ? [sdk.client.global.config.update({ config: { indexing: global } })] : []),
|
||||
...(indexing.enabled !== undefined
|
||||
? [sdk.client.config.update({ config: { indexing: { enabled: indexing.enabled } } })]
|
||||
: []),
|
||||
const patch = indexingPatch(before, indexing)
|
||||
const response = await sdk.client.config.overlayUpdate({
|
||||
scope,
|
||||
set: { indexing: patch.indexing },
|
||||
unset: patch.unset,
|
||||
})
|
||||
if (response.error) {
|
||||
toast.show({ message: "Failed to save indexing config", variant: "error" })
|
||||
return false
|
||||
}
|
||||
const [configResponse, globalResponse] = await Promise.all([
|
||||
sdk.client.config.get({}),
|
||||
sdk.client.global.config.get({}),
|
||||
])
|
||||
if (responses.some((response) => response.error)) {
|
||||
toast.show({ message: "Failed to save indexing config", variant: "error" })
|
||||
return false
|
||||
}
|
||||
const configResponse = await sdk.client.config.get({})
|
||||
if (configResponse.data) {
|
||||
sync.set("config", reconcile(configResponse.data))
|
||||
}
|
||||
toast.show({ message: "Indexing config saved", variant: "success" })
|
||||
return true
|
||||
}
|
||||
|
||||
async function saveGlobalIndexing(
|
||||
sdk: SDK,
|
||||
sync: ReturnType<typeof useSync>,
|
||||
indexing: IndexingConfig,
|
||||
toast: ReturnType<typeof useToast>,
|
||||
): Promise<boolean> {
|
||||
const response = await sdk.client.global.config.update({ config: { indexing } })
|
||||
if (response.error) {
|
||||
toast.show({ message: "Failed to save indexing config", variant: "error" })
|
||||
return false
|
||||
}
|
||||
const merged = await sdk.client.config.get({})
|
||||
if (merged.data) sync.set("config", reconcile(merged.data))
|
||||
toast.show({ message: "Indexing config saved", variant: "success" })
|
||||
return true
|
||||
}
|
||||
|
||||
async function saveProjectIndexing(
|
||||
sdk: SDK,
|
||||
sync: ReturnType<typeof useSync>,
|
||||
indexing: IndexingConfig,
|
||||
toast: ReturnType<typeof useToast>,
|
||||
): Promise<boolean> {
|
||||
const response = await sdk.client.config.update({ config: { indexing: { enabled: indexing.enabled } } })
|
||||
if (response.error) {
|
||||
toast.show({ message: "Failed to save indexing config", variant: "error" })
|
||||
return false
|
||||
}
|
||||
const configResponse = await sdk.client.config.get({})
|
||||
if (configResponse.data) sync.set("config", reconcile(configResponse.data))
|
||||
if (globalResponse.data) sync.set("globalConfig", reconcile(globalResponse.data))
|
||||
toast.show({ message: "Indexing config saved", variant: "success" })
|
||||
return true
|
||||
}
|
||||
|
||||
function providerSettingsDescription(
|
||||
sync: ReturnType<typeof useSync>,
|
||||
indexing: IndexingConfig,
|
||||
provider: EmbeddingProvider,
|
||||
): string {
|
||||
if (provider === "kilo") return hasKiloAuth(sync) ? "uses Kilo account" : "sign in to Kilo"
|
||||
function providerSettingsDescription(indexing: IndexingConfig, provider: EmbeddingProvider): string {
|
||||
const fields = PROVIDER_FIELDS[provider]
|
||||
const settings = indexing[provider] as Record<string, string | undefined> | undefined
|
||||
if (!settings) return "not configured"
|
||||
@@ -175,6 +153,10 @@ function providerSettingsDescription(
|
||||
|
||||
interface SubDialogProps {
|
||||
useSDK: () => UseSDK
|
||||
scope: IndexingScope
|
||||
indexing: IndexingConfig
|
||||
raw: IndexingConfig
|
||||
global?: IndexingConfig
|
||||
}
|
||||
|
||||
function ProviderSelect(props: SubDialogProps) {
|
||||
@@ -182,12 +164,12 @@ function ProviderSelect(props: SubDialogProps) {
|
||||
const sync = useSync()
|
||||
const sdk = props.useSDK()
|
||||
const toast = useToast()
|
||||
const indexing = defaultIndexing(sync)
|
||||
const indexing = props.indexing
|
||||
|
||||
const options: DialogSelectOption<EmbeddingProvider>[] = (
|
||||
Object.entries(PROVIDER_LABELS) as [EmbeddingProvider, string][]
|
||||
)
|
||||
.filter(([value]) => value !== "kilo" || hasKiloAuth(sync) || indexing.provider === "kilo")
|
||||
.filter(([value]) => value !== "kilo" || hasKiloAuth(sync, props.scope, indexing) || indexing.provider === "kilo")
|
||||
.map(([value, title]) => ({
|
||||
value,
|
||||
title,
|
||||
@@ -201,19 +183,18 @@ function ProviderSelect(props: SubDialogProps) {
|
||||
current={indexing.provider}
|
||||
onSelect={async (option) => {
|
||||
const provider = option.value
|
||||
const current = getIndexing(sync)
|
||||
const updated: IndexingConfig = {
|
||||
...current,
|
||||
...props.raw,
|
||||
provider,
|
||||
model: null,
|
||||
dimension: null,
|
||||
}
|
||||
const saved = await saveIndexing(sdk, sync, updated, toast)
|
||||
const saved = await saveScopedIndexing(sdk, sync, props.scope, props.raw, updated, toast)
|
||||
if (!saved) {
|
||||
dialog.clear()
|
||||
return
|
||||
}
|
||||
showProviderSettings(dialog, sync, sdk, toast, provider, props.useSDK)
|
||||
showProviderSettings(dialog, sync, sdk, toast, provider, props.useSDK, props.scope, updated, updated)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -224,51 +205,57 @@ function KiloModelSelect(props: SubDialogProps) {
|
||||
const sync = useSync()
|
||||
const sdk = props.useSDK()
|
||||
const toast = useToast()
|
||||
const indexing = defaultIndexing(sync)
|
||||
const provider = sync.data.provider_next.all.find((item) => item.id === "kilo")
|
||||
const auth = resolveKiloIndexingAuth({ config: sync.data.config, provider })
|
||||
const [catalog] = createResource(() => fetchKiloEmbeddingModelCatalog({ baseURL: auth.baseUrl, token: auth.apiKey }))
|
||||
const options = () =>
|
||||
(catalog()?.models ?? []).map((model) => ({
|
||||
value: model.id,
|
||||
title: formatKiloEmbeddingModelLabel(model),
|
||||
}))
|
||||
const current = () => {
|
||||
const indexing = props.indexing
|
||||
const [error, setError] = createSignal<string>()
|
||||
const [catalog] = createResource(() => loadKiloEmbeddingModels(setError))
|
||||
const seen = { error: undefined as string | undefined, state: "" }
|
||||
createEffect(() => {
|
||||
const message = error()
|
||||
if (!message || seen.error === message) return
|
||||
seen.error = message
|
||||
toast.show({
|
||||
title: "Code Indexing Error",
|
||||
message,
|
||||
variant: "error",
|
||||
duration: 10000,
|
||||
})
|
||||
})
|
||||
createEffect(() => {
|
||||
const cfg = catalog()
|
||||
if (!cfg) return undefined
|
||||
const fallback = cfg.aliases[cfg.defaultModel] ?? cfg.defaultModel
|
||||
const id = indexing.model ? (cfg.aliases[indexing.model] ?? indexing.model) : fallback
|
||||
if (cfg.models.some((model) => model.id === id)) return id
|
||||
return fallback
|
||||
}
|
||||
const state = `${catalog.state}:${cfg?.models.length ?? 0}`
|
||||
if (seen.state === state) return
|
||||
seen.state = state
|
||||
log.info("Kilo embedding model resource changed", {
|
||||
state: catalog.state,
|
||||
models: cfg?.models.length ?? 0,
|
||||
current: currentKiloModel(cfg, indexing.model),
|
||||
defaultModel: cfg?.defaultModel || undefined,
|
||||
scope: props.scope,
|
||||
})
|
||||
})
|
||||
const options = createMemo(() => kiloModelOptions(catalog()))
|
||||
const current = createMemo(() => currentKiloModel(catalog(), indexing.model))
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={!catalog.loading && options().length > 0}
|
||||
fallback={
|
||||
<DialogSelect
|
||||
title="Kilo Embedding Model"
|
||||
options={[
|
||||
{
|
||||
value: "",
|
||||
title: catalog.loading ? "Loading supported models..." : "No supported models available",
|
||||
disabled: true,
|
||||
},
|
||||
]}
|
||||
renderFilter={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DialogSelect
|
||||
title="Kilo Embedding Model"
|
||||
options={options()}
|
||||
current={current()}
|
||||
onSelect={async (option) => {
|
||||
await saveIndexing(sdk, sync, { ...getIndexing(sync), model: option.value, dimension: null }, toast)
|
||||
dialog.replace(() => <DialogIndexing useSDK={props.useSDK} />)
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
<DialogSelect
|
||||
title="Kilo Embedding Model"
|
||||
options={options()}
|
||||
current={current()}
|
||||
renderFilter={(catalog()?.models.length ?? 0) > 0}
|
||||
onSelect={async (option) => {
|
||||
if (!option.value || !catalog()?.models.some((model) => model.id === option.value)) return
|
||||
log.info("selected Kilo embedding model", { model: option.value, scope: props.scope })
|
||||
await saveScopedIndexing(
|
||||
sdk,
|
||||
sync,
|
||||
props.scope,
|
||||
props.raw,
|
||||
{ ...props.raw, model: option.value, dimension: null },
|
||||
toast,
|
||||
)
|
||||
dialog.replace(() => <DialogIndexing useSDK={props.useSDK} scope={props.scope} />)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -279,13 +266,15 @@ async function showProviderSettings(
|
||||
toast: ReturnType<typeof useToast>,
|
||||
provider: EmbeddingProvider,
|
||||
useSDK: () => UseSDK,
|
||||
scope: IndexingScope,
|
||||
indexing: IndexingConfig,
|
||||
raw: IndexingConfig,
|
||||
) {
|
||||
const fields = PROVIDER_FIELDS[provider]
|
||||
if (fields.length === 0) {
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} />)
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} scope={scope} />)
|
||||
return
|
||||
}
|
||||
const indexing = defaultIndexing(sync)
|
||||
const currentSettings = (indexing[provider] as Record<string, string | undefined>) ?? {}
|
||||
const newSettings: Record<string, string | undefined> = { ...currentSettings }
|
||||
|
||||
@@ -296,15 +285,15 @@ async function showProviderSettings(
|
||||
placeholder: field.placeholder,
|
||||
})
|
||||
if (result === null) {
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} />)
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} scope={scope} />)
|
||||
return
|
||||
}
|
||||
newSettings[field.key] = result.trim() || undefined
|
||||
}
|
||||
|
||||
const updated = { ...getIndexing(sync), [provider]: newSettings }
|
||||
await saveIndexing(sdk, sync, updated, toast)
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} />)
|
||||
const updated = { ...raw, [provider]: newSettings }
|
||||
await saveScopedIndexing(sdk, sync, scope, raw, updated, toast)
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} scope={scope} />)
|
||||
}
|
||||
|
||||
function VectorStoreSelect(props: SubDialogProps) {
|
||||
@@ -312,7 +301,7 @@ function VectorStoreSelect(props: SubDialogProps) {
|
||||
const sync = useSync()
|
||||
const sdk = props.useSDK()
|
||||
const toast = useToast()
|
||||
const indexing = defaultIndexing(sync)
|
||||
const indexing = props.indexing
|
||||
|
||||
const options: DialogSelectOption<string>[] = Object.entries(VECTOR_STORE_LABELS).map(([value, title]) => ({
|
||||
value,
|
||||
@@ -328,9 +317,9 @@ function VectorStoreSelect(props: SubDialogProps) {
|
||||
onSelect={async (option) => {
|
||||
const store = option.value as "lancedb" | "qdrant"
|
||||
if (store === "lancedb") {
|
||||
await showLancedbSettings(dialog, sync, sdk, toast, props.useSDK)
|
||||
await showLancedbSettings(dialog, sync, sdk, toast, props.useSDK, props.scope, indexing, props.raw)
|
||||
} else {
|
||||
await showQdrantSettings(dialog, sync, sdk, toast, props.useSDK)
|
||||
await showQdrantSettings(dialog, sync, sdk, toast, props.useSDK, props.scope, indexing, props.raw)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -343,23 +332,25 @@ async function showLancedbSettings(
|
||||
sdk: SDK,
|
||||
toast: ReturnType<typeof useToast>,
|
||||
useSDK: () => UseSDK,
|
||||
scope: IndexingScope,
|
||||
indexing: IndexingConfig,
|
||||
raw: IndexingConfig,
|
||||
) {
|
||||
const indexing = getIndexing(sync)
|
||||
const result = await DialogPrompt.show(dialog, "LanceDB — Directory", {
|
||||
value: indexing.lancedb?.directory ?? "",
|
||||
placeholder: "Leave empty for default",
|
||||
})
|
||||
if (result === null) {
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} />)
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} scope={scope} />)
|
||||
return
|
||||
}
|
||||
const updated: IndexingConfig = {
|
||||
...getIndexing(sync),
|
||||
...raw,
|
||||
vectorStore: "lancedb",
|
||||
lancedb: { directory: result.trim() || undefined },
|
||||
}
|
||||
await saveIndexing(sdk, sync, updated, toast)
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} />)
|
||||
await saveScopedIndexing(sdk, sync, scope, raw, updated, toast)
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} scope={scope} />)
|
||||
}
|
||||
|
||||
async function showQdrantSettings(
|
||||
@@ -368,8 +359,10 @@ async function showQdrantSettings(
|
||||
sdk: SDK,
|
||||
toast: ReturnType<typeof useToast>,
|
||||
useSDK: () => UseSDK,
|
||||
scope: IndexingScope,
|
||||
indexing: IndexingConfig,
|
||||
raw: IndexingConfig,
|
||||
) {
|
||||
const indexing = getIndexing(sync)
|
||||
const currentSettings = indexing.qdrant ?? {}
|
||||
|
||||
const url = await DialogPrompt.show(dialog, "Qdrant — URL", {
|
||||
@@ -377,7 +370,7 @@ async function showQdrantSettings(
|
||||
placeholder: "http://localhost:6333",
|
||||
})
|
||||
if (url === null) {
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} />)
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} scope={scope} />)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -386,20 +379,20 @@ async function showQdrantSettings(
|
||||
placeholder: "Optional API key",
|
||||
})
|
||||
if (apiKey === null) {
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} />)
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} scope={scope} />)
|
||||
return
|
||||
}
|
||||
|
||||
const updated: IndexingConfig = {
|
||||
...getIndexing(sync),
|
||||
...raw,
|
||||
vectorStore: "qdrant",
|
||||
qdrant: {
|
||||
url: url.trim() || undefined,
|
||||
apiKey: apiKey.trim() || undefined,
|
||||
},
|
||||
}
|
||||
await saveIndexing(sdk, sync, updated, toast)
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} />)
|
||||
await saveScopedIndexing(sdk, sync, scope, raw, updated, toast)
|
||||
dialog.replace(() => <DialogIndexing useSDK={useSDK} scope={scope} />)
|
||||
}
|
||||
|
||||
interface TuningParam {
|
||||
@@ -423,14 +416,16 @@ function TuningMenu(props: SubDialogProps) {
|
||||
const sync = useSync()
|
||||
const sdk = props.useSDK()
|
||||
const toast = useToast()
|
||||
const indexing = getIndexing(sync)
|
||||
const indexing = props.indexing
|
||||
|
||||
const options: DialogSelectOption<string>[] = TUNING_PARAMS.map((param) => {
|
||||
const value = indexing[param.key]
|
||||
const description = value !== undefined ? String(value) : `default (${param.defaultValue})`
|
||||
const inheritance = indexingInheritance(props.scope, props.global ?? {}, props.raw, [[param.key]])
|
||||
return {
|
||||
value: param.key,
|
||||
title: param.label,
|
||||
description: value !== undefined ? String(value) : `default (${param.defaultValue})`,
|
||||
description: inheritedDescription(description, inheritance),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -440,26 +435,50 @@ function TuningMenu(props: SubDialogProps) {
|
||||
options={options}
|
||||
onSelect={async (option) => {
|
||||
const param = TUNING_PARAMS.find((p) => p.key === option.value)!
|
||||
const currentIndexing = getIndexing(sync)
|
||||
const currentValue = currentIndexing[param.key]
|
||||
const currentValue = indexing[param.key]
|
||||
const result = await DialogPrompt.show(dialog, param.label, {
|
||||
value: currentValue !== undefined ? String(currentValue) : "",
|
||||
placeholder: `Default: ${param.defaultValue}`,
|
||||
})
|
||||
if (result === null) {
|
||||
dialog.replace(() => <TuningMenu useSDK={props.useSDK} />)
|
||||
dialog.replace(() => (
|
||||
<TuningMenu
|
||||
useSDK={props.useSDK}
|
||||
scope={props.scope}
|
||||
indexing={indexing}
|
||||
raw={props.raw}
|
||||
global={props.global}
|
||||
/>
|
||||
))
|
||||
return
|
||||
}
|
||||
const trimmed = result.trim()
|
||||
const num = trimmed ? Number(trimmed) : undefined
|
||||
if (trimmed && isNaN(num!)) {
|
||||
toast.show({ message: `Invalid number: "${trimmed}"`, variant: "error" })
|
||||
dialog.replace(() => <TuningMenu useSDK={props.useSDK} />)
|
||||
dialog.replace(() => (
|
||||
<TuningMenu
|
||||
useSDK={props.useSDK}
|
||||
scope={props.scope}
|
||||
indexing={indexing}
|
||||
raw={props.raw}
|
||||
global={props.global}
|
||||
/>
|
||||
))
|
||||
return
|
||||
}
|
||||
const updated = { ...getIndexing(sync), [param.key]: num }
|
||||
await saveIndexing(sdk, sync, updated, toast)
|
||||
dialog.replace(() => <TuningMenu useSDK={props.useSDK} />)
|
||||
const updated = { ...props.raw, [param.key]: num }
|
||||
await saveScopedIndexing(sdk, sync, props.scope, props.raw, updated, toast)
|
||||
const effective = props.scope === "project" ? mergeIndexingConfig(props.global ?? {}, updated) : updated
|
||||
dialog.replace(() => (
|
||||
<TuningMenu
|
||||
useSDK={props.useSDK}
|
||||
scope={props.scope}
|
||||
indexing={effective}
|
||||
raw={updated}
|
||||
global={props.global}
|
||||
/>
|
||||
))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -469,6 +488,26 @@ function TuningMenu(props: SubDialogProps) {
|
||||
|
||||
interface DialogIndexingProps {
|
||||
useSDK: () => UseSDK
|
||||
scope?: IndexingScope
|
||||
}
|
||||
|
||||
function ScopeSelect(props: DialogIndexingProps & { scope: IndexingScope }) {
|
||||
const dialog = useDialog()
|
||||
const options: DialogSelectOption<IndexingScope>[] = [
|
||||
{ value: "global", title: "Global", description: "Stored in the user config directory" },
|
||||
{ value: "project", title: "Project", description: "Stored in this repo's .kilo config" },
|
||||
]
|
||||
|
||||
return (
|
||||
<DialogSelect
|
||||
title="Indexing Scope"
|
||||
options={options}
|
||||
current={props.scope}
|
||||
onSelect={(option) => {
|
||||
dialog.replace(() => <DialogIndexing useSDK={props.useSDK} scope={option.value} />)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function DialogIndexing(props: DialogIndexingProps) {
|
||||
@@ -476,117 +515,131 @@ export function DialogIndexing(props: DialogIndexingProps) {
|
||||
const sync = useSync()
|
||||
const sdk = props.useSDK()
|
||||
const toast = useToast()
|
||||
const [global] = createResource(async () => (await sdk.client.global.config.get({})).data as Config | undefined)
|
||||
const globalCfg = () => globalIndexing(global())
|
||||
const indexing = defaultIndexing(sync, globalCfg())
|
||||
const scope = () => props.scope ?? "global"
|
||||
const [overlay] = createResource(async () => (await sdk.client.config.overlay({ scope: "project" })).data)
|
||||
const globalCfg = () => scopedIndexing((overlay()?.global as Config | undefined) ?? sync.data.globalConfig)
|
||||
const projectCfg = () => scopedIndexing(overlay()?.project as Config | undefined)
|
||||
const state = createIndexingDialogState({
|
||||
scope,
|
||||
global: globalCfg,
|
||||
project: projectCfg,
|
||||
resolve: (current, global) => defaultIndexing(sync, scope(), current, global),
|
||||
})
|
||||
const options = createMemo<DialogSelectOption<string>[]>(() => {
|
||||
const indexing = state.config()
|
||||
const provider = indexing.provider ? PROVIDER_LABELS[indexing.provider] : "not set"
|
||||
const store = indexing.vectorStore ?? DEFAULT_VECTOR_STORE
|
||||
const storeLabel = VECTOR_STORE_LABELS[store] ?? store
|
||||
const mark = (value: string, paths: readonly (readonly string[])[]) =>
|
||||
inheritedDescription(value, state.inherited(paths))
|
||||
const count = TUNING_PARAMS.filter((param) => indexing[param.key] !== undefined).length
|
||||
const tuning = count > 0 ? `${count} customized` : "defaults"
|
||||
const tuningPaths = TUNING_PARAMS.map((param) => [param.key])
|
||||
const result: DialogSelectOption<string>[] = [
|
||||
{
|
||||
value: "scope",
|
||||
title: "Configuration Scope",
|
||||
category: "General",
|
||||
description: scope(),
|
||||
},
|
||||
{
|
||||
value: "enabled",
|
||||
title: "Indexing",
|
||||
category: "General",
|
||||
description: mark(state.enabled() ? "enabled" : "disabled", [["enabled"]]),
|
||||
},
|
||||
{
|
||||
value: "provider",
|
||||
title: "Embedding Provider",
|
||||
category: "Embedding",
|
||||
description: mark(provider, [["provider"]]),
|
||||
},
|
||||
{
|
||||
value: "model",
|
||||
title: "Embedding Model",
|
||||
category: "Embedding",
|
||||
description: mark(
|
||||
indexing.provider === "kilo" ? (indexing.model ?? "Kilo catalog") : (indexing.model ?? "default"),
|
||||
[["model"]],
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "dimension",
|
||||
title: "Vector Dimension",
|
||||
category: "Embedding",
|
||||
description:
|
||||
indexing.provider === "kilo"
|
||||
? "provided by Kilo"
|
||||
: mark(indexing.dimension ? String(indexing.dimension) : "auto", [["dimension"]]),
|
||||
disabled: indexing.provider === "kilo",
|
||||
},
|
||||
{
|
||||
value: "vectorStore",
|
||||
title: "Vector Store",
|
||||
category: "Storage",
|
||||
description: mark(storeLabel, [["vectorStore"]]),
|
||||
},
|
||||
{
|
||||
value: "tuning",
|
||||
title: "Tuning Parameters",
|
||||
category: "Advanced",
|
||||
description: mark(tuning, tuningPaths),
|
||||
},
|
||||
]
|
||||
|
||||
const providerLabel = indexing.provider ? PROVIDER_LABELS[indexing.provider] : "not set"
|
||||
const store = indexing.vectorStore ?? DEFAULT_VECTOR_STORE
|
||||
const storeLabel = VECTOR_STORE_LABELS[store] ?? store
|
||||
|
||||
const tuningCount = TUNING_PARAMS.filter((p) => indexing[p.key] !== undefined).length
|
||||
const tuningDesc = tuningCount > 0 ? `${tuningCount} customized` : "defaults"
|
||||
|
||||
const options: DialogSelectOption<string>[] = [
|
||||
{
|
||||
value: "globalToggle",
|
||||
title: "Indexing (Global)",
|
||||
category: "General",
|
||||
description: global.loading ? "loading" : globalCfg().enabled ? "enabled" : "disabled",
|
||||
},
|
||||
{
|
||||
value: "projectToggle",
|
||||
title: "Indexing (Project)",
|
||||
category: "General",
|
||||
description: globalCfg().enabled ? "controlled by global" : indexing.enabled ? "enabled" : "disabled",
|
||||
},
|
||||
{
|
||||
value: "provider",
|
||||
title: "Embedding Provider",
|
||||
category: "Embedding",
|
||||
description: providerLabel,
|
||||
},
|
||||
{
|
||||
value: "model",
|
||||
title: indexing.provider === "kilo" ? "Kilo Model Preset" : "Embedding Model",
|
||||
category: "Embedding",
|
||||
description: indexing.provider === "kilo" ? "hosted presets only" : (indexing.model ?? "default"),
|
||||
},
|
||||
{
|
||||
value: "dimension",
|
||||
title: "Vector Dimension",
|
||||
category: "Embedding",
|
||||
description: indexing.dimension ? String(indexing.dimension) : "auto",
|
||||
},
|
||||
{
|
||||
value: "vectorStore",
|
||||
title: "Vector Store",
|
||||
category: "Storage",
|
||||
description: storeLabel,
|
||||
},
|
||||
{
|
||||
value: "tuning",
|
||||
title: "Tuning Parameters",
|
||||
category: "Advanced",
|
||||
description: tuningDesc,
|
||||
},
|
||||
]
|
||||
|
||||
if (indexing.provider) {
|
||||
const settingsDesc = providerSettingsDescription(sync, indexing, indexing.provider)
|
||||
options.splice(2, 0, {
|
||||
value: "providerSettings",
|
||||
title: `${PROVIDER_LABELS[indexing.provider]} Settings`,
|
||||
category: "Embedding",
|
||||
description: settingsDesc,
|
||||
})
|
||||
}
|
||||
if (indexing.provider && PROVIDER_FIELDS[indexing.provider].length > 0) {
|
||||
result.splice(3, 0, {
|
||||
value: "providerSettings",
|
||||
title: `${PROVIDER_LABELS[indexing.provider]} Settings`,
|
||||
category: "Embedding",
|
||||
description: mark(
|
||||
providerSettingsDescription(indexing, indexing.provider),
|
||||
PROVIDER_FIELDS[indexing.provider].map((field) => [indexing.provider!, field.key]),
|
||||
),
|
||||
})
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
return (
|
||||
<DialogSelect
|
||||
title="Indexing Configuration"
|
||||
options={options}
|
||||
options={options()}
|
||||
skipFilter
|
||||
onSelect={async (option) => {
|
||||
const indexing = state.config()
|
||||
const raw = state.raw()
|
||||
switch (option.value) {
|
||||
case "globalToggle": {
|
||||
const enabled = !globalCfg().enabled
|
||||
const updated =
|
||||
enabled && !globalCfg().provider && !getIndexing(sync).provider && hasKiloAuth(sync)
|
||||
? { ...defaultIndexing(sync, globalCfg()), enabled }
|
||||
: { enabled }
|
||||
await saveGlobalIndexing(sdk, sync, updated, toast)
|
||||
dialog.replace(() => <DialogIndexing useSDK={props.useSDK} />)
|
||||
case "scope":
|
||||
dialog.replace(() => <ScopeSelect useSDK={props.useSDK} scope={scope()} />)
|
||||
break
|
||||
}
|
||||
case "projectToggle": {
|
||||
if (globalCfg().enabled) {
|
||||
toast.show({
|
||||
message: "Global indexing is enabled, so this project is already covered.",
|
||||
variant: "info",
|
||||
})
|
||||
dialog.replace(() => <DialogIndexing useSDK={props.useSDK} />)
|
||||
break
|
||||
}
|
||||
const current = getIndexing(sync)
|
||||
const enabled = !indexing.enabled
|
||||
const updated =
|
||||
enabled && !current.provider && hasKiloAuth(sync) ? { ...defaultIndexing(sync), enabled } : { enabled }
|
||||
await saveProjectIndexing(sdk, sync, updated, toast)
|
||||
dialog.replace(() => <DialogIndexing useSDK={props.useSDK} />)
|
||||
case "enabled":
|
||||
await saveScopedIndexing(sdk, sync, scope(), raw, { ...raw, enabled: !state.enabled() }, toast)
|
||||
dialog.replace(() => <DialogIndexing useSDK={props.useSDK} scope={scope()} />)
|
||||
break
|
||||
}
|
||||
case "provider":
|
||||
dialog.replace(() => <ProviderSelect useSDK={props.useSDK} />)
|
||||
dialog.replace(() => <ProviderSelect useSDK={props.useSDK} scope={scope()} indexing={indexing} raw={raw} />)
|
||||
break
|
||||
case "providerSettings":
|
||||
if (indexing.provider) {
|
||||
await showProviderSettings(dialog, sync, sdk, toast, indexing.provider, props.useSDK)
|
||||
await showProviderSettings(
|
||||
dialog,
|
||||
sync,
|
||||
sdk,
|
||||
toast,
|
||||
indexing.provider,
|
||||
props.useSDK,
|
||||
scope(),
|
||||
indexing,
|
||||
raw,
|
||||
)
|
||||
}
|
||||
break
|
||||
case "model": {
|
||||
if (indexing.provider === "kilo") {
|
||||
dialog.replace(() => <KiloModelSelect useSDK={props.useSDK} />)
|
||||
dialog.replace(() => (
|
||||
<KiloModelSelect useSDK={props.useSDK} scope={scope()} indexing={indexing} raw={raw} />
|
||||
))
|
||||
break
|
||||
}
|
||||
const result = await DialogPrompt.show(dialog, "Embedding Model", {
|
||||
@@ -595,12 +648,13 @@ export function DialogIndexing(props: DialogIndexingProps) {
|
||||
})
|
||||
if (result !== null) {
|
||||
const trimmed = result.trim()
|
||||
await saveIndexing(sdk, sync, { ...getIndexing(sync), model: trimmed || null }, toast)
|
||||
await saveScopedIndexing(sdk, sync, scope(), raw, { ...raw, model: trimmed || null }, toast)
|
||||
}
|
||||
dialog.replace(() => <DialogIndexing useSDK={props.useSDK} />)
|
||||
dialog.replace(() => <DialogIndexing useSDK={props.useSDK} scope={scope()} />)
|
||||
break
|
||||
}
|
||||
case "dimension": {
|
||||
if (indexing.provider === "kilo") break
|
||||
const result = await DialogPrompt.show(dialog, "Vector Dimension", {
|
||||
value: indexing.dimension ? String(indexing.dimension) : "",
|
||||
placeholder: "Leave empty for auto-detection",
|
||||
@@ -612,21 +666,24 @@ export function DialogIndexing(props: DialogIndexingProps) {
|
||||
dim = Number(trimmed)
|
||||
if (isNaN(dim) || dim <= 0 || !Number.isInteger(dim)) {
|
||||
toast.show({ message: `Invalid dimension: "${trimmed}"`, variant: "error" })
|
||||
dialog.replace(() => <DialogIndexing useSDK={props.useSDK} />)
|
||||
dialog.replace(() => <DialogIndexing useSDK={props.useSDK} scope={scope()} />)
|
||||
break
|
||||
}
|
||||
}
|
||||
const updated = { ...getIndexing(sync), dimension: dim ?? null }
|
||||
await saveIndexing(sdk, sync, updated, toast)
|
||||
await saveScopedIndexing(sdk, sync, scope(), raw, { ...raw, dimension: dim ?? null }, toast)
|
||||
}
|
||||
dialog.replace(() => <DialogIndexing useSDK={props.useSDK} />)
|
||||
dialog.replace(() => <DialogIndexing useSDK={props.useSDK} scope={scope()} />)
|
||||
break
|
||||
}
|
||||
case "vectorStore":
|
||||
dialog.replace(() => <VectorStoreSelect useSDK={props.useSDK} />)
|
||||
dialog.replace(() => (
|
||||
<VectorStoreSelect useSDK={props.useSDK} scope={scope()} indexing={indexing} raw={raw} />
|
||||
))
|
||||
break
|
||||
case "tuning":
|
||||
dialog.replace(() => <TuningMenu useSDK={props.useSDK} />)
|
||||
dialog.replace(() => (
|
||||
<TuningMenu useSDK={props.useSDK} scope={scope()} indexing={indexing} raw={raw} global={globalCfg()} />
|
||||
))
|
||||
break
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { fetchKiloEmbeddingModelCatalog, resolveKiloGatewayBaseUrl } from "@kilocode/kilo-gateway"
|
||||
import type { Config, IndexingConfig, KiloEmbeddingModelCatalog } from "@kilocode/sdk/v2"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { createMemo, type Accessor } from "solid-js"
|
||||
|
||||
export type IndexingScope = "global" | "project"
|
||||
|
||||
const log = Log.create({ service: "indexing-model-catalog" })
|
||||
|
||||
export async function loadKiloEmbeddingModels(onError?: (message: string) => void) {
|
||||
const endpoint = new URL("embedding-models", resolveKiloGatewayBaseUrl()).toString()
|
||||
log.info("loading Kilo embedding model catalog", { endpoint })
|
||||
const catalog = await fetchKiloEmbeddingModelCatalog({
|
||||
onError: (issue) => {
|
||||
log.warn("failed to load Kilo embedding model catalog", {
|
||||
code: issue.code,
|
||||
status: issue.status,
|
||||
message: issue.message,
|
||||
})
|
||||
onError?.(issue.message)
|
||||
},
|
||||
})
|
||||
log.info("loaded Kilo embedding model catalog", {
|
||||
models: catalog.models.length,
|
||||
defaultModel: catalog.defaultModel || undefined,
|
||||
})
|
||||
return catalog
|
||||
}
|
||||
|
||||
export function kiloModelOptions(catalog?: KiloEmbeddingModelCatalog) {
|
||||
if (!catalog) return [{ value: "", title: "Loading supported models..." }]
|
||||
if (catalog.models.length === 0) return [{ value: "", title: "No supported models available" }]
|
||||
return catalog.models.map((model) => ({
|
||||
value: model.id,
|
||||
title: `${model.name} (${model.note ? `${model.note}, ` : ""}${model.dimension}d)`,
|
||||
}))
|
||||
}
|
||||
|
||||
export function currentKiloModel(catalog: KiloEmbeddingModelCatalog | undefined, model?: string | null) {
|
||||
if (!catalog) return undefined
|
||||
const fallback = catalog.aliases[catalog.defaultModel] ?? catalog.defaultModel
|
||||
const current = model ? (catalog.aliases[model] ?? model) : fallback
|
||||
return catalog.models.some((item) => item.id === current) ? current : fallback
|
||||
}
|
||||
|
||||
export function indexingScopeConfig(
|
||||
scope: IndexingScope,
|
||||
effective: Config,
|
||||
global: Config,
|
||||
indexing: IndexingConfig,
|
||||
): Config {
|
||||
return { ...(scope === "global" ? global : effective), indexing }
|
||||
}
|
||||
|
||||
function record(input: unknown): input is Record<string, unknown> {
|
||||
return typeof input === "object" && input !== null && !Array.isArray(input)
|
||||
}
|
||||
|
||||
function get(input: IndexingConfig, path: readonly string[]) {
|
||||
return path.reduce<unknown>((value, key) => (record(value) ? value[key] : undefined), input)
|
||||
}
|
||||
|
||||
export type IndexingInheritance = "none" | "inherited" | "partial"
|
||||
|
||||
export function indexingInheritance(
|
||||
scope: IndexingScope,
|
||||
global: IndexingConfig,
|
||||
project: IndexingConfig,
|
||||
paths: readonly (readonly string[])[],
|
||||
): IndexingInheritance {
|
||||
if (scope !== "project") return "none"
|
||||
const configured = paths.filter((path) => get(global, path) !== undefined || get(project, path) !== undefined)
|
||||
const inherited = configured.filter((path) => get(project, path) === undefined && get(global, path) !== undefined)
|
||||
if (inherited.length === 0) return "none"
|
||||
return inherited.length === configured.length ? "inherited" : "partial"
|
||||
}
|
||||
|
||||
export function inheritedDescription(value: string, inheritance: IndexingInheritance) {
|
||||
if (inheritance === "inherited") return `${value} (inherited)`
|
||||
if (inheritance === "partial") return `${value} (partially inherited)`
|
||||
return value
|
||||
}
|
||||
|
||||
function prune(input: unknown): unknown {
|
||||
if (!record(input)) return input
|
||||
const entries = Object.entries(input).flatMap(([key, value]) => {
|
||||
if (value === undefined) return []
|
||||
const next = prune(value)
|
||||
if (record(next) && Object.keys(next).length === 0) return []
|
||||
return [[key, next] as const]
|
||||
})
|
||||
return Object.fromEntries(entries)
|
||||
}
|
||||
|
||||
function removed(before: unknown, after: unknown, prefix: string[]): string[][] {
|
||||
if (!record(before)) return []
|
||||
const next = record(after) ? after : {}
|
||||
return Object.entries(before).flatMap(([key, value]) => {
|
||||
const path = [...prefix, key]
|
||||
if (!(key in next)) return [path]
|
||||
if (record(value) && record(next[key])) return removed(value, next[key], path)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
export function indexingPatch(before: IndexingConfig, after: IndexingConfig) {
|
||||
const indexing = prune(after) as IndexingConfig
|
||||
const unset = removed(before, indexing, ["indexing"])
|
||||
return { indexing, unset: unset.length > 0 ? unset : undefined }
|
||||
}
|
||||
|
||||
function mergeRecord(base: Record<string, unknown>, patch: Record<string, unknown>) {
|
||||
const result: Record<string, unknown> = { ...base }
|
||||
for (const [key, value] of Object.entries(patch)) {
|
||||
if (value === undefined) continue
|
||||
if (record(value) && record(result[key])) {
|
||||
result[key] = mergeRecord(result[key], value)
|
||||
continue
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function mergeIndexingConfig(base: IndexingConfig, patch: IndexingConfig): IndexingConfig {
|
||||
return mergeRecord(base, patch) as IndexingConfig
|
||||
}
|
||||
|
||||
export function createIndexingDialogState(input: {
|
||||
scope: Accessor<IndexingScope>
|
||||
global: Accessor<IndexingConfig>
|
||||
project: Accessor<IndexingConfig>
|
||||
resolve: (indexing: IndexingConfig, global: IndexingConfig) => IndexingConfig
|
||||
}) {
|
||||
const raw = createMemo(() => (input.scope() === "global" ? input.global() : input.project()))
|
||||
const config = createMemo(() => {
|
||||
const value = input.scope() === "global" ? raw() : mergeIndexingConfig(input.global(), raw())
|
||||
return input.resolve(value, input.global())
|
||||
})
|
||||
const enabled = createMemo(() => {
|
||||
if (input.scope() === "global") return raw().enabled === true
|
||||
return (raw().enabled ?? input.global().enabled) === true
|
||||
})
|
||||
const inherited = (paths: readonly (readonly string[])[]) =>
|
||||
indexingInheritance(input.scope(), input.global(), input.project(), paths)
|
||||
|
||||
return { raw, config, enabled, inherited }
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export namespace KilocodeConfig {
|
||||
*
|
||||
* This mirrors the Kilo project-config load chain: prefer existing config files
|
||||
* in ancestor config directories, then existing root config files, and create
|
||||
* `.kilo/kilo.json` when no project config exists yet.
|
||||
* `.kilo/kilo.jsonc` when no project config exists yet.
|
||||
*/
|
||||
export const projectConfigUpdateTarget = Effect.fn("KilocodeConfig.projectConfigUpdateTarget")(function* (input: {
|
||||
fs: AppFileSystem.Interface
|
||||
@@ -79,7 +79,7 @@ export namespace KilocodeConfig {
|
||||
.up({ targets: [...ALL_CONFIG_FILES], start: input.directory, stop: input.worktree })
|
||||
.pipe(Effect.orDie)
|
||||
const files = [...dirs.flatMap((dir) => ALL_CONFIG_FILES.map((file) => path.join(dir, file))), ...roots]
|
||||
return files.find((file) => existsSync(file)) ?? path.join(input.directory, ".kilo", "kilo.json")
|
||||
return files.find((file) => existsSync(file)) ?? path.join(input.directory, ".kilo", "kilo.jsonc")
|
||||
})
|
||||
|
||||
export const updateProjectConfig = Effect.fn("KilocodeConfig.updateProjectConfig")(function* (input: {
|
||||
@@ -98,6 +98,7 @@ export namespace KilocodeConfig {
|
||||
const patch = input.writable(input.config)
|
||||
|
||||
if (file.endsWith(".jsonc")) {
|
||||
if (source === undefined && Object.keys(mergeConfig({}, patch)).length === 0) return
|
||||
const updated = input.patch(before, patch)
|
||||
yield* input.fs.writeWithDirs(file, updated).pipe(Effect.orDie)
|
||||
return
|
||||
|
||||
@@ -84,6 +84,33 @@ export namespace KilocodeConfigOverlay {
|
||||
["disabled_providers"],
|
||||
["watcher", "ignore"],
|
||||
["instructions"],
|
||||
["indexing", "enabled"],
|
||||
["indexing", "provider"],
|
||||
["indexing", "model"],
|
||||
["indexing", "dimension"],
|
||||
["indexing", "vectorStore"],
|
||||
["indexing", "kilo", "apiKey"],
|
||||
["indexing", "kilo", "baseUrl"],
|
||||
["indexing", "kilo", "organizationId"],
|
||||
["indexing", "openai", "apiKey"],
|
||||
["indexing", "ollama", "baseUrl"],
|
||||
["indexing", "openai-compatible", "baseUrl"],
|
||||
["indexing", "openai-compatible", "apiKey"],
|
||||
["indexing", "gemini", "apiKey"],
|
||||
["indexing", "mistral", "apiKey"],
|
||||
["indexing", "vercel-ai-gateway", "apiKey"],
|
||||
["indexing", "bedrock", "region"],
|
||||
["indexing", "bedrock", "profile"],
|
||||
["indexing", "openrouter", "apiKey"],
|
||||
["indexing", "openrouter", "specificProvider"],
|
||||
["indexing", "voyage", "apiKey"],
|
||||
["indexing", "qdrant", "url"],
|
||||
["indexing", "qdrant", "apiKey"],
|
||||
["indexing", "lancedb", "directory"],
|
||||
["indexing", "searchMinScore"],
|
||||
["indexing", "searchMaxResults"],
|
||||
["indexing", "embeddingBatchSize"],
|
||||
["indexing", "scannerMaxBatchRetries"],
|
||||
] as const
|
||||
|
||||
const collectionPaths = ["provider", "mcp", "permission", "agent", "formatter", "lsp"] as const
|
||||
@@ -99,7 +126,7 @@ export namespace KilocodeConfigOverlay {
|
||||
const found = await Filesystem.findUp([...dirs], input.directory, input.worktree)
|
||||
const roots = await Filesystem.findUp([...files], input.directory, input.worktree)
|
||||
const candidates = [...found.flatMap((dir) => files.map((file) => path.join(dir, file))), ...roots]
|
||||
return candidates.find((file) => existsSync(file)) ?? path.join(input.directory, ".kilo", "kilo.json")
|
||||
return candidates.find((file) => existsSync(file)) ?? path.join(input.directory, ".kilo", "kilo.jsonc")
|
||||
}
|
||||
|
||||
export function globalTarget() {
|
||||
@@ -188,19 +215,46 @@ export namespace KilocodeConfigOverlay {
|
||||
parts: string[],
|
||||
): Resolved {
|
||||
const key = parts.join(".")
|
||||
const value = fieldValue(scope, effective, global, local, parts)
|
||||
const hasValue = hasFieldValue(scope, effective, global, local, parts)
|
||||
return resolved({
|
||||
key,
|
||||
path: parts,
|
||||
scope,
|
||||
value: get(effective, parts),
|
||||
value,
|
||||
global: get(global, parts),
|
||||
local: get(local, parts),
|
||||
hasValue: has(effective, parts),
|
||||
hasValue,
|
||||
hasGlobal: has(global, parts),
|
||||
hasLocal: has(local, parts),
|
||||
})
|
||||
}
|
||||
|
||||
function isIndexing(parts: string[]) {
|
||||
return parts[0] === "indexing"
|
||||
}
|
||||
|
||||
function fieldValue(scope: Scope, effective: Config.Info, global: Config.Info, local: Config.Info, parts: string[]) {
|
||||
if (!isIndexing(parts)) return get(effective, parts)
|
||||
if (scope === "project" && has(local, parts)) return get(local, parts)
|
||||
if (has(global, parts)) return get(global, parts)
|
||||
if (scope === "global" && has(local, parts)) return undefined
|
||||
return get(effective, parts)
|
||||
}
|
||||
|
||||
function hasFieldValue(
|
||||
scope: Scope,
|
||||
effective: Config.Info,
|
||||
global: Config.Info,
|
||||
local: Config.Info,
|
||||
parts: string[],
|
||||
) {
|
||||
if (!isIndexing(parts)) return has(effective, parts)
|
||||
if (scope === "project" && has(local, parts)) return true
|
||||
if (has(global, parts)) return true
|
||||
return !has(local, parts) && has(effective, parts)
|
||||
}
|
||||
|
||||
function collection(scope: Scope, effective: Config.Info, global: Config.Info, local: Config.Info, key: string) {
|
||||
const names = new Set([
|
||||
...Object.keys(record(get(effective, [key]))),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Schema } from "effect"
|
||||
import { INDEXING_STATUS_STATES } from "@kilocode/kilo-indexing/status"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
import { INDEXING_WARNING_CODES } from "./indexing-warning"
|
||||
|
||||
export const IndexingStatusState = Schema.Literals(INDEXING_STATUS_STATES).annotate({
|
||||
identifier: "IndexingStatusState",
|
||||
@@ -21,3 +22,10 @@ export const Event = BusEvent.define(
|
||||
status: IndexingStatusInfo,
|
||||
}),
|
||||
)
|
||||
|
||||
export const IndexingWarningInfo = Schema.Struct({
|
||||
code: Schema.Literals(INDEXING_WARNING_CODES),
|
||||
message: Schema.String,
|
||||
}).annotate({ identifier: "IndexingWarning" })
|
||||
|
||||
export const Warning = BusEvent.define("indexing.warning", IndexingWarningInfo)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { IndexingStatus } from "@kilocode/kilo-indexing/status"
|
||||
|
||||
export const INDEXING_WARNING_CODES = ["qdrant.version-incompatible", "qdrant.version-unavailable"] as const
|
||||
|
||||
export type IndexingWarning = {
|
||||
code: (typeof INDEXING_WARNING_CODES)[number]
|
||||
message: string
|
||||
}
|
||||
|
||||
const incompatible =
|
||||
/^Client version .+ is incompatible with server version .+\. Major versions should match and minor version difference must not exceed 1\. Set checkCompatibility=false to skip version check\.$/
|
||||
const detail = " Major versions should match and minor version difference must not exceed 1."
|
||||
const unavailable =
|
||||
/^Failed to obtain server version\. Unable to check client-server compatibility\. Set checkCompatibility=false to skip version check\.$/
|
||||
|
||||
export function parseQdrantWarning(value: unknown): IndexingWarning | undefined {
|
||||
if (typeof value !== "string") return undefined
|
||||
if (incompatible.test(value)) return { code: "qdrant.version-incompatible", message: value.replace(detail, "") }
|
||||
if (unavailable.test(value)) return { code: "qdrant.version-unavailable", message: value }
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function indexingWarningKey(warning: IndexingWarning): string {
|
||||
return `${warning.code}\u0000${warning.message}`
|
||||
}
|
||||
|
||||
export function indexingErrorMessage(status: IndexingStatus): string | undefined {
|
||||
return status.state === "Error" ? status.message : undefined
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import type {
|
||||
} from "@kilocode/kilo-indexing/engine"
|
||||
import type { IndexingStatus } from "@kilocode/kilo-indexing/status"
|
||||
import { withTimeout } from "@/util/timeout"
|
||||
import type { Message, Request, Result } from "./indexing-worker-protocol"
|
||||
import type { Log, Message, Request, Result } from "./indexing-worker-protocol"
|
||||
import type { IndexingWarning } from "./indexing-warning"
|
||||
|
||||
declare global {
|
||||
const KILO_INDEXING_WORKER_PATH: string
|
||||
@@ -15,6 +16,8 @@ export namespace IndexingWorker {
|
||||
export type Hooks = {
|
||||
status(status: IndexingStatus): void
|
||||
telemetry(event: IndexingTelemetryEvent): void
|
||||
warning(warning: IndexingWarning): void
|
||||
log(event: Log): void
|
||||
failure(err: unknown): void
|
||||
}
|
||||
|
||||
@@ -26,16 +29,24 @@ export namespace IndexingWorker {
|
||||
|
||||
export type Factory = (directory: string, root: string, hooks: Hooks) => Driver
|
||||
|
||||
const worker = (directory: string, root: string, hooks: Hooks): Driver => {
|
||||
type Host = Driver & {
|
||||
use(hooks: Hooks): void
|
||||
}
|
||||
|
||||
const pool = new Map<string, Host>()
|
||||
|
||||
const worker = (directory: string, root: string, hooks: Hooks): Host => {
|
||||
const file =
|
||||
typeof KILO_INDEXING_WORKER_PATH !== "undefined"
|
||||
? KILO_INDEXING_WORKER_PATH
|
||||
: new URL("./indexing-worker.ts", import.meta.url)
|
||||
const task = new Worker(file)
|
||||
const key = `${directory}\0${root}`
|
||||
const task = new Worker(file, { ref: false })
|
||||
const pending = new Map<number, { resolve(message: Result): void; reject(err: unknown): void }>()
|
||||
let id = 0
|
||||
let stopped = false
|
||||
let stopping = false
|
||||
let active = true
|
||||
let callbacks = hooks
|
||||
|
||||
const reject = (err: unknown) => {
|
||||
for (const item of pending.values()) item.reject(err)
|
||||
@@ -43,19 +54,22 @@ export namespace IndexingWorker {
|
||||
}
|
||||
|
||||
const fail = (err: unknown) => {
|
||||
if (stopped || stopping) return
|
||||
if (stopped) return
|
||||
stopped = true
|
||||
active = false
|
||||
reject(err)
|
||||
task.terminate()
|
||||
hooks.failure(err)
|
||||
if (pool.get(key) === host) pool.delete(key)
|
||||
callbacks.failure(err)
|
||||
}
|
||||
|
||||
task.onmessage = (event: MessageEvent<Message>) => {
|
||||
const message = event.data
|
||||
if (message.type === "event") {
|
||||
if (stopping || stopped) return
|
||||
if (message.event === "status") hooks.status(message.data)
|
||||
if (message.event === "telemetry") hooks.telemetry(message.data)
|
||||
if (stopped || !active) return
|
||||
if (message.event === "status") callbacks.status(message.data)
|
||||
if (message.event === "telemetry") callbacks.telemetry(message.data)
|
||||
if (message.event === "warning") callbacks.warning(message.data)
|
||||
if (message.event === "log") callbacks.log(message.data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -73,8 +87,13 @@ export namespace IndexingWorker {
|
||||
fail(event.error ?? new Error(event.message))
|
||||
}
|
||||
|
||||
const call = <T>(request: Request, read: (message: Result) => T, allowStopping = false) => {
|
||||
if (stopped || (stopping && !allowStopping)) return Promise.reject(new Error("Indexing worker is disposed."))
|
||||
task.addEventListener("close", () => {
|
||||
if (pool.get(key) === host) pool.delete(key)
|
||||
fail(new Error("Indexing worker exited."))
|
||||
})
|
||||
|
||||
const call = <T>(request: Request, read: (message: Result) => T) => {
|
||||
if (stopped) return Promise.reject(new Error("Indexing worker is unavailable."))
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
pending.set(request.id, {
|
||||
resolve(message) {
|
||||
@@ -90,8 +109,13 @@ export namespace IndexingWorker {
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
const host: Host = {
|
||||
use(next) {
|
||||
callbacks = next
|
||||
active = true
|
||||
},
|
||||
init(config) {
|
||||
active = true
|
||||
const request: Request = {
|
||||
type: "request",
|
||||
id: id++,
|
||||
@@ -116,35 +140,41 @@ export namespace IndexingWorker {
|
||||
})
|
||||
},
|
||||
async dispose() {
|
||||
if (stopped || stopping) return
|
||||
stopping = true
|
||||
if (stopped || !active) return
|
||||
active = false
|
||||
const request: Request = { type: "request", id: id++, method: "dispose", input: undefined }
|
||||
await withTimeout(
|
||||
call(
|
||||
request,
|
||||
(message) => {
|
||||
if (message.ok && message.method === "dispose") return message.value
|
||||
throw new Error("Unexpected indexing worker dispose response.")
|
||||
},
|
||||
true,
|
||||
),
|
||||
call(request, (message) => {
|
||||
if (message.ok && message.method === "dispose") return message.value
|
||||
throw new Error("Unexpected indexing worker dispose response.")
|
||||
}),
|
||||
1000,
|
||||
"Indexing worker shutdown timed out",
|
||||
).catch(() => undefined)
|
||||
stopped = true
|
||||
reject(new Error("Indexing worker is disposed."))
|
||||
task.terminate()
|
||||
"Indexing worker reset timed out",
|
||||
).catch((err) => {
|
||||
stopped = true
|
||||
reject(err)
|
||||
})
|
||||
},
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
let factory: Factory = worker
|
||||
let factory: Factory | undefined
|
||||
|
||||
export function create(directory: string, root: string, hooks: Hooks) {
|
||||
return factory(directory, root, hooks)
|
||||
if (factory) return factory(directory, root, hooks)
|
||||
const key = `${directory}\0${root}`
|
||||
const existing = pool.get(key)
|
||||
if (existing) {
|
||||
existing.use(hooks)
|
||||
return existing
|
||||
}
|
||||
const next = worker(directory, root, hooks)
|
||||
pool.set(key, next)
|
||||
return next
|
||||
}
|
||||
|
||||
export function override(next?: Factory) {
|
||||
factory = next ?? worker
|
||||
factory = next
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
VectorStoreSearchResult,
|
||||
} from "@kilocode/kilo-indexing/engine"
|
||||
import type { IndexingStatus } from "@kilocode/kilo-indexing/status"
|
||||
import type { IndexingWarning } from "./indexing-warning"
|
||||
|
||||
export type InitInput = {
|
||||
directory: string
|
||||
@@ -23,8 +24,15 @@ export type Result =
|
||||
| { type: "result"; id: number; method: "dispose"; ok: true; value: undefined }
|
||||
| { type: "result"; id: number; method: Request["method"]; ok: false; error: string }
|
||||
|
||||
export type Log = {
|
||||
level: "debug" | "info" | "warn" | "error"
|
||||
message: string
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| { type: "event"; event: "status"; data: IndexingStatus }
|
||||
| { type: "event"; event: "telemetry"; data: IndexingTelemetryEvent }
|
||||
| { type: "event"; event: "warning"; data: IndexingWarning }
|
||||
| { type: "event"; event: "log"; data: Log }
|
||||
|
||||
export type Message = Result | Event
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CodeIndexManager } from "@kilocode/kilo-indexing/engine"
|
||||
import { normalizeIndexingStatus } from "@kilocode/kilo-indexing/status"
|
||||
import type { Request, Result, Event } from "./indexing-worker-protocol"
|
||||
import type { CodeIndexManager } from "@kilocode/kilo-indexing/engine"
|
||||
import { format } from "node:util"
|
||||
import type { Request, Result, Event, Log } from "./indexing-worker-protocol"
|
||||
import { parseQdrantWarning } from "./indexing-warning"
|
||||
|
||||
let manager: CodeIndexManager | undefined
|
||||
let progress: { dispose(): void } | undefined
|
||||
@@ -10,6 +11,20 @@ function send(message: Result | Event) {
|
||||
postMessage(message)
|
||||
}
|
||||
|
||||
function write(level: Log["level"], args: unknown[]) {
|
||||
const message = format(...args)
|
||||
send({ type: "event", event: "log", data: { level, message } })
|
||||
if (level !== "warn") return
|
||||
const warning = parseQdrantWarning(message)
|
||||
if (warning) send({ type: "event", event: "warning", data: warning })
|
||||
}
|
||||
|
||||
console.debug = (...args) => write("debug", args)
|
||||
console.info = (...args) => write("info", args)
|
||||
console.log = (...args) => write("info", args)
|
||||
console.warn = (...args) => write("warn", args)
|
||||
console.error = (...args) => write("error", args)
|
||||
|
||||
function dispose() {
|
||||
progress?.dispose()
|
||||
telemetry?.dispose()
|
||||
@@ -22,20 +37,23 @@ function dispose() {
|
||||
async function init(request: Extract<Request, { method: "init" }>) {
|
||||
dispose()
|
||||
if (request.input.lancedbPath) process.env.KILO_LANCEDB_PATH = request.input.lancedbPath
|
||||
const next = new CodeIndexManager(request.input.directory, request.input.root)
|
||||
const [engine, status] = await Promise.all([
|
||||
import("@kilocode/kilo-indexing/engine"),
|
||||
import("@kilocode/kilo-indexing/status"),
|
||||
])
|
||||
const next = new engine.CodeIndexManager(request.input.directory, request.input.root)
|
||||
manager = next
|
||||
progress = next.onProgressUpdate.on(() => {
|
||||
send({ type: "event", event: "status", data: normalizeIndexingStatus(next) })
|
||||
send({ type: "event", event: "status", data: status.normalizeIndexingStatus(next) })
|
||||
})
|
||||
telemetry = next.onTelemetry.on((data) => {
|
||||
send({ type: "event", event: "telemetry", data })
|
||||
})
|
||||
await next.initialize(request.input.config)
|
||||
send({ type: "result", id: request.id, method: "init", ok: true, value: normalizeIndexingStatus(next) })
|
||||
send({ type: "result", id: request.id, method: "init", ok: true, value: status.normalizeIndexingStatus(next) })
|
||||
}
|
||||
|
||||
onmessage = async (event: MessageEvent<Request>) => {
|
||||
const request = event.data
|
||||
async function handle(request: Request) {
|
||||
try {
|
||||
if (request.method === "dispose") {
|
||||
dispose()
|
||||
@@ -55,3 +73,8 @@ onmessage = async (event: MessageEvent<Request>) => {
|
||||
send({ type: "result", id: request.id, method: request.method, ok: false, error })
|
||||
}
|
||||
}
|
||||
|
||||
let queue = Promise.resolve()
|
||||
onmessage = (event: MessageEvent<Request>) => {
|
||||
queue = queue.then(() => handle(event.data))
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ import { makeRuntime } from "@/effect/run-service"
|
||||
import { registerDisposer } from "@/effect/instance-registry"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Event as IndexingEvent } from "./indexing-event"
|
||||
import type { WorkspaceID } from "@/control-plane/schema"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
import { Event as IndexingEvent, Warning as IndexingWarningEvent } from "./indexing-event"
|
||||
import { indexingWarningKey, type IndexingWarning } from "./indexing-warning"
|
||||
import { IndexingWorker } from "./indexing-worker-client"
|
||||
import { LanceDBRuntime } from "./lancedb" // kilocode_change
|
||||
import { indexingWithKiloDefault, resolveKiloIndexingAuth, type KiloIndexingAuth } from "./indexing-auth" // kilocode_change
|
||||
@@ -105,7 +108,7 @@ async function model(input: ReturnType<typeof toIndexingConfigInput>, auth: Kilo
|
||||
return {
|
||||
...input,
|
||||
modelId: found.id,
|
||||
modelDimension: chosen ? (input.modelDimension ?? found.dimension) : found.dimension,
|
||||
modelDimension: found.dimension,
|
||||
searchMinScore: input.searchMinScore ?? found.scoreThreshold,
|
||||
}
|
||||
}
|
||||
@@ -188,7 +191,7 @@ export namespace KiloIndexing {
|
||||
export function input(config?: IndexingConfig, global?: IndexingConfig) {
|
||||
return toIndexingConfigInput({
|
||||
...config,
|
||||
enabled: config?.enabled === true || global?.enabled === true,
|
||||
enabled: config?.enabled ?? global?.enabled ?? false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -196,6 +199,8 @@ export namespace KiloIndexing {
|
||||
engine?: IndexingWorker.Driver
|
||||
initialized?: boolean
|
||||
current(): Status
|
||||
warnings(): IndexingWarning[]
|
||||
scope(workspace: WorkspaceID | undefined): void
|
||||
publish(): Promise<void>
|
||||
dispose(): Promise<void>
|
||||
}
|
||||
@@ -210,6 +215,7 @@ export namespace KiloIndexing {
|
||||
}
|
||||
|
||||
export const Event = IndexingEvent
|
||||
export const Warning = IndexingWarningEvent
|
||||
|
||||
const cache = new Map<string, Cache>()
|
||||
|
||||
@@ -220,6 +226,8 @@ export namespace KiloIndexing {
|
||||
|
||||
return {
|
||||
current,
|
||||
warnings: () => [],
|
||||
scope() {},
|
||||
publish,
|
||||
async dispose() {},
|
||||
}
|
||||
@@ -253,35 +261,99 @@ export namespace KiloIndexing {
|
||||
const global = globalConfig.indexing
|
||||
const merged = indexingWithKiloDefault({ ...global, ...cfg.indexing }, auth)
|
||||
const cfgInput = await model(enrichKilo(input(merged, global), auth), auth)
|
||||
const workspaces = new Set<WorkspaceID | undefined>([WorkspaceContext.workspaceID])
|
||||
const box = { status: pending() }
|
||||
const warnings = new Map<string, IndexingWarning>()
|
||||
const delivery = {
|
||||
last: undefined as Status | undefined,
|
||||
task: Promise.resolve(),
|
||||
timer: undefined as ReturnType<typeof setTimeout> | undefined,
|
||||
time: 0,
|
||||
}
|
||||
const current = () => box.status
|
||||
let disposed = false
|
||||
|
||||
const publish = async () => {
|
||||
await Bus.publish(Event, { status: current() })
|
||||
}
|
||||
const report = Instance.bind(async () => {
|
||||
try {
|
||||
return await publish()
|
||||
} catch (err) {
|
||||
log.error("failed to publish indexing status", { err })
|
||||
}
|
||||
const same = (left: Status | undefined, right: Status) =>
|
||||
left?.state === right.state &&
|
||||
left.message === right.message &&
|
||||
left.processedFiles === right.processedFiles &&
|
||||
left.totalFiles === right.totalFiles &&
|
||||
left.percent === right.percent
|
||||
const report = Instance.bind((next = current()) => {
|
||||
delivery.task = delivery.task
|
||||
.then(async () => {
|
||||
if (disposed || same(delivery.last, next)) return
|
||||
await Bus.publish(Event, { status: next })
|
||||
delivery.last = next
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to publish indexing status", { err })
|
||||
})
|
||||
return delivery.task
|
||||
})
|
||||
const clear = () => {
|
||||
if (!delivery.timer) return
|
||||
clearTimeout(delivery.timer)
|
||||
delivery.timer = undefined
|
||||
}
|
||||
const status = Instance.bind((next: Status) => {
|
||||
if (disposed) return
|
||||
const previous = current()
|
||||
box.status = next
|
||||
void report()
|
||||
if (same(previous, next)) return
|
||||
const immediate = previous.state !== next.state || next.state !== "In Progress"
|
||||
if (immediate) {
|
||||
clear()
|
||||
delivery.time = Date.now()
|
||||
void report(next)
|
||||
return
|
||||
}
|
||||
if (delivery.timer) return
|
||||
const delay = Math.max(0, 250 - (Date.now() - delivery.time))
|
||||
if (delay === 0) {
|
||||
delivery.time = Date.now()
|
||||
void report(next)
|
||||
return
|
||||
}
|
||||
delivery.timer = setTimeout(
|
||||
Instance.bind(() => {
|
||||
delivery.timer = undefined
|
||||
delivery.time = Date.now()
|
||||
void report()
|
||||
}),
|
||||
delay,
|
||||
)
|
||||
})
|
||||
const telemetry = Instance.bind((event: IndexingTelemetryEvent) => {
|
||||
if (disposed) return
|
||||
trackTelemetry(event)
|
||||
})
|
||||
const warning = Instance.bind((item: IndexingWarning) => {
|
||||
if (disposed) return
|
||||
const key = indexingWarningKey(item)
|
||||
if (warnings.has(key)) return
|
||||
warnings.set(key, item)
|
||||
void Promise.all(
|
||||
[...workspaces].map((workspaceID) =>
|
||||
WorkspaceContext.provide({ workspaceID, fn: () => Bus.publish(Warning, item) }),
|
||||
),
|
||||
).catch((err) => {
|
||||
log.error("failed to publish indexing warning", { err, workspacePath: dir })
|
||||
})
|
||||
})
|
||||
const output = Instance.bind((event: Parameters<IndexingWorker.Hooks["log"]>[0]) => {
|
||||
if (disposed) return
|
||||
log[event.level](event.message, { source: "worker", workspacePath: dir })
|
||||
})
|
||||
const base: Entry = {
|
||||
current,
|
||||
publish,
|
||||
warnings: () => [...warnings.values()],
|
||||
scope: (workspaceID) => workspaces.add(workspaceID),
|
||||
publish: () => report(),
|
||||
async dispose() {
|
||||
if (disposed) return
|
||||
disposed = true
|
||||
clear()
|
||||
base.initialized = false
|
||||
await base.engine?.dispose().catch((err) => {
|
||||
log.warn("failed to dispose project indexing worker", { err, workspacePath: dir })
|
||||
@@ -291,9 +363,8 @@ export namespace KiloIndexing {
|
||||
const failure = Instance.bind((err: unknown) => {
|
||||
if (disposed) return
|
||||
base.initialized = false
|
||||
box.status = failed(err)
|
||||
log.error("project indexing worker failed", { err, workspacePath: dir })
|
||||
void report()
|
||||
status(failed(err))
|
||||
})
|
||||
track(hit, base)
|
||||
await report()
|
||||
@@ -309,7 +380,7 @@ export namespace KiloIndexing {
|
||||
const err = await LanceDBRuntime.ensure(cfgInput.vectorStoreProvider)
|
||||
.then(async () => {
|
||||
if (hit.disposed) return
|
||||
const engine = IndexingWorker.create(dir, root, { status, telemetry, failure })
|
||||
const engine = IndexingWorker.create(dir, root, { status, telemetry, warning, log: output, failure })
|
||||
base.engine = engine
|
||||
box.status = await engine.init(cfgInput)
|
||||
base.initialized = true
|
||||
@@ -325,12 +396,13 @@ export namespace KiloIndexing {
|
||||
log.warn("failed to dispose failed project indexing worker", { err: disposeErr, workspacePath: dir })
|
||||
})
|
||||
base.engine = undefined
|
||||
box.status = failed(err)
|
||||
const next = failed(err)
|
||||
status(next)
|
||||
log.error("project indexing initialization failed", {
|
||||
err,
|
||||
workspacePath: dir,
|
||||
})
|
||||
await report()
|
||||
await report(next)
|
||||
return base
|
||||
}
|
||||
|
||||
@@ -391,7 +463,29 @@ export namespace KiloIndexing {
|
||||
}
|
||||
|
||||
export async function current(): Promise<Status> {
|
||||
return (await hit().ready).current()
|
||||
const entry = await hit().ready
|
||||
entry.scope(WorkspaceContext.workspaceID)
|
||||
return entry.current()
|
||||
}
|
||||
|
||||
export async function models() {
|
||||
try {
|
||||
const cfg = await AppRuntime.runPromise(Config.Service.use((svc) => svc.getGlobal()))
|
||||
const auth = await kiloAuth(cfg)
|
||||
const catalog = await fetchKiloEmbeddingModelCatalog({ baseURL: auth.baseUrl, token: auth.apiKey })
|
||||
if (catalog.models.length > 0 || (!auth.baseUrl && !auth.apiKey)) return catalog
|
||||
const fallback = await fetchKiloEmbeddingModelCatalog()
|
||||
return fallback.models.length > 0 ? fallback : catalog
|
||||
} catch (err) {
|
||||
log.warn("falling back to public Kilo embedding model catalog", { err })
|
||||
return fetchKiloEmbeddingModelCatalog()
|
||||
}
|
||||
}
|
||||
|
||||
export async function warnings(): Promise<IndexingWarning[]> {
|
||||
const entry = await hit().ready
|
||||
entry.scope(WorkspaceContext.workspaceID)
|
||||
return entry.warnings()
|
||||
}
|
||||
|
||||
export function ready(): boolean {
|
||||
@@ -402,12 +496,14 @@ export namespace KiloIndexing {
|
||||
|
||||
export async function available(): Promise<boolean> {
|
||||
const entry = await hit().ready
|
||||
entry.scope(WorkspaceContext.workspaceID)
|
||||
if (!entry.initialized) return false
|
||||
return entry.current().state !== "Disabled"
|
||||
}
|
||||
|
||||
export async function search(query: string, directoryPrefix?: string): Promise<VectorStoreSearchResult[]> {
|
||||
const entry = await hit().ready
|
||||
entry.scope(WorkspaceContext.workspaceID)
|
||||
if (!entry.initialized || entry.current().state === "Disabled" || !entry.engine) return []
|
||||
return entry.engine.search(query, directoryPrefix)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { IndexingStatusInfo } from "@/kilocode/indexing-event"
|
||||
import { IndexingStatusInfo, IndexingWarningInfo } from "@/kilocode/indexing-event"
|
||||
import { Authorization } from "@/server/routes/instance/httpapi/middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "@/server/routes/instance/httpapi/middleware/instance-context"
|
||||
import {
|
||||
@@ -8,12 +9,28 @@ import {
|
||||
} from "@/server/routes/instance/httpapi/middleware/workspace-routing"
|
||||
import { described } from "@/server/routes/instance/httpapi/groups/metadata"
|
||||
|
||||
export { IndexingStatusInfo, IndexingStatusState } from "@/kilocode/indexing-event"
|
||||
export { IndexingStatusInfo, IndexingStatusState, IndexingWarningInfo } from "@/kilocode/indexing-event"
|
||||
|
||||
export const KiloEmbeddingModel = Schema.Struct({
|
||||
id: Schema.String,
|
||||
name: Schema.String,
|
||||
dimension: Schema.Int.check(Schema.isGreaterThan(0)),
|
||||
scoreThreshold: Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 1 })),
|
||||
note: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
export const KiloEmbeddingModelCatalog = Schema.Struct({
|
||||
defaultModel: Schema.String,
|
||||
models: Schema.Array(KiloEmbeddingModel),
|
||||
aliases: Schema.Record(Schema.String, Schema.String),
|
||||
}).annotate({ identifier: "KiloEmbeddingModelCatalog" })
|
||||
|
||||
const root = "/indexing"
|
||||
|
||||
export const IndexingPaths = {
|
||||
status: `${root}/status`,
|
||||
models: `${root}/models`,
|
||||
warnings: `${root}/warnings`,
|
||||
} as const
|
||||
|
||||
export const IndexingApi = HttpApi.make("indexing")
|
||||
@@ -30,6 +47,28 @@ export const IndexingApi = HttpApi.make("indexing")
|
||||
description: "Retrieve the current code indexing status for the active project.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("warnings", IndexingPaths.warnings, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(IndexingWarningInfo), "Indexing warnings"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "indexing.warnings",
|
||||
summary: "Get indexing warnings",
|
||||
description: "Retrieve code indexing warnings for the active project.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("models", IndexingPaths.models, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(KiloEmbeddingModelCatalog, "Kilo embedding model catalog"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "indexing.models",
|
||||
summary: "List Kilo embedding models",
|
||||
description: "Retrieve the embedding models available through the active Kilo account.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
||||
@@ -10,6 +10,7 @@ import { KilocodeKeybinds } from "@/kilocode/tui/keybinds"
|
||||
import { KilocodeTuiConfig } from "@/kilocode/tui/config"
|
||||
import { disposeAllInstancesAndEmitGlobalDisposed } from "@/server/global-lifecycle"
|
||||
import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api"
|
||||
import { markInstanceForDisposal } from "@/server/routes/instance/httpapi/lifecycle"
|
||||
import { Effect, Option } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import {
|
||||
@@ -87,6 +88,7 @@ export const configConsoleHandlers = HttpApiBuilder.group(InstanceHttpApi, "conf
|
||||
return result.info
|
||||
}
|
||||
yield* config.update(patch)
|
||||
yield* markInstanceForDisposal(yield* InstanceState.context)
|
||||
return yield* config.get()
|
||||
})
|
||||
|
||||
|
||||
@@ -5,12 +5,17 @@ import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api"
|
||||
|
||||
export const indexingHandlers = HttpApiBuilder.group(InstanceHttpApi, "indexing", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const mod = yield* Effect.promise(() => import("@/kilocode/indexing"))
|
||||
const status = Effect.fn("IndexingHttpApi.status")(function* () {
|
||||
const mod = yield* Effect.promise(() => import("@/kilocode/indexing"))
|
||||
const current = yield* EffectBridge.fromPromise(() => mod.KiloIndexing.current())
|
||||
return current
|
||||
return yield* EffectBridge.fromPromise(() => mod.KiloIndexing.current())
|
||||
})
|
||||
const models = Effect.fn("IndexingHttpApi.models")(function* () {
|
||||
return yield* EffectBridge.fromPromise(() => mod.KiloIndexing.models())
|
||||
})
|
||||
const warnings = Effect.fn("IndexingHttpApi.warnings")(function* () {
|
||||
return yield* EffectBridge.fromPromise(() => mod.KiloIndexing.warnings())
|
||||
})
|
||||
|
||||
return handlers.handle("status", status)
|
||||
return handlers.handle("status", status).handle("models", models).handle("warnings", warnings)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -49,6 +49,8 @@ const clear = () =>
|
||||
Effect.runPromise(Config.Service.use((svc) => svc.invalidate()).pipe(Effect.scoped, Effect.provide(layer)))
|
||||
const saveGlobal = (config: Config.Info) =>
|
||||
Effect.runPromise(Config.Service.use((svc) => svc.updateGlobal(config)).pipe(Effect.scoped, Effect.provide(layer)))
|
||||
const saveProject = (config: Config.Info) =>
|
||||
Effect.runPromise(Config.Service.use((svc) => svc.update(config)).pipe(Effect.scoped, Effect.provide(layer)))
|
||||
|
||||
async function writeConfig(dir: string, config: object, name = "kilo.json") {
|
||||
await Filesystem.write(path.join(dir, name), JSON.stringify(config))
|
||||
@@ -191,9 +193,25 @@ describe("kilocode indexing config", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("global indexing enabled applies when project indexing is disabled", async () => {
|
||||
test("project indexing enabled overrides global enablement", async () => {
|
||||
const input = KiloIndexing.input({ enabled: false }, { enabled: true })
|
||||
expect(input.enabled).toBe(true)
|
||||
expect(input.enabled).toBe(false)
|
||||
expect(KiloIndexing.input(undefined, { enabled: true }).enabled).toBe(true)
|
||||
expect(KiloIndexing.input({ enabled: true }, { enabled: false }).enabled).toBe(true)
|
||||
})
|
||||
|
||||
test("creates missing project config as .kilo/kilo.jsonc", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await saveProject({ indexing: { enabled: true } })
|
||||
},
|
||||
})
|
||||
|
||||
expect(await Bun.file(path.join(tmp.path, ".kilo", "kilo.jsonc")).exists()).toBe(true)
|
||||
expect(await Bun.file(path.join(tmp.path, ".kilo", "kilo.json")).exists()).toBe(false)
|
||||
})
|
||||
|
||||
test("accepts delete sentinels for indexing model overrides", () => {
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createEffect, createRoot, createSignal } from "solid-js"
|
||||
import type { Config, IndexingConfig } from "@kilocode/sdk/v2"
|
||||
import {
|
||||
createIndexingDialogState,
|
||||
currentKiloModel,
|
||||
indexingInheritance,
|
||||
indexingPatch,
|
||||
indexingScopeConfig,
|
||||
inheritedDescription,
|
||||
kiloModelOptions,
|
||||
loadKiloEmbeddingModels,
|
||||
mergeIndexingConfig,
|
||||
type IndexingScope,
|
||||
} from "../../src/kilocode/components/indexing-dialog-state"
|
||||
|
||||
describe("indexing dialog state", () => {
|
||||
test.serial("loads Kilo models directly from the public catalog", async () => {
|
||||
const original = global.fetch
|
||||
const calls: string[] = []
|
||||
global.fetch = (async (input) => {
|
||||
calls.push(String(input))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
defaultModel: "kilo/default",
|
||||
models: [{ id: "kilo/default", name: "Default", dimension: 1024, scoreThreshold: 0.35 }],
|
||||
aliases: {},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
)
|
||||
}) as typeof global.fetch
|
||||
|
||||
try {
|
||||
const catalog = await loadKiloEmbeddingModels()
|
||||
|
||||
expect(catalog.models).toHaveLength(1)
|
||||
expect(catalog.defaultModel).toBe("kilo/default")
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(new URL(calls[0] ?? "https://invalid.test").pathname).toEndWith("/embedding-models")
|
||||
} finally {
|
||||
global.fetch = original
|
||||
}
|
||||
})
|
||||
|
||||
test("builds stable loading, empty, and loaded model options", () => {
|
||||
expect(kiloModelOptions()).toEqual([{ value: "", title: "Loading supported models..." }])
|
||||
expect(kiloModelOptions({ defaultModel: "", models: [], aliases: {} })).toEqual([
|
||||
{ value: "", title: "No supported models available" },
|
||||
])
|
||||
|
||||
const catalog = {
|
||||
defaultModel: "provider/default",
|
||||
models: [
|
||||
{ id: "provider/default", name: "Default", dimension: 1024, scoreThreshold: 0.35 },
|
||||
{ id: "provider/code", name: "Code", dimension: 1536, scoreThreshold: 0.4, note: "code" },
|
||||
],
|
||||
aliases: { code: "provider/code" },
|
||||
}
|
||||
|
||||
expect(kiloModelOptions(catalog)).toEqual([
|
||||
{ value: "provider/default", title: "Default (1024d)" },
|
||||
{ value: "provider/code", title: "Code (code, 1536d)" },
|
||||
])
|
||||
expect(currentKiloModel(catalog, "code")).toBe("provider/code")
|
||||
expect(currentKiloModel(catalog, "missing")).toBe("provider/default")
|
||||
})
|
||||
|
||||
test("classifies scalar and partial nested inheritance", () => {
|
||||
const global: IndexingConfig = {
|
||||
provider: "openai-compatible",
|
||||
model: "global-model",
|
||||
dimension: 1024,
|
||||
"openai-compatible": { baseUrl: "https://global.test", apiKey: "global-secret" },
|
||||
}
|
||||
const project: IndexingConfig = {
|
||||
model: null,
|
||||
"openai-compatible": { baseUrl: "https://project.test" },
|
||||
}
|
||||
|
||||
expect(indexingInheritance("project", global, project, [["provider"]])).toBe("inherited")
|
||||
expect(indexingInheritance("project", global, project, [["model"]])).toBe("none")
|
||||
expect(indexingInheritance("project", global, project, [["dimension"]])).toBe("inherited")
|
||||
expect(
|
||||
indexingInheritance("project", global, project, [
|
||||
["openai-compatible", "baseUrl"],
|
||||
["openai-compatible", "apiKey"],
|
||||
]),
|
||||
).toBe("partial")
|
||||
expect(indexingInheritance("global", global, project, [["provider"]])).toBe("none")
|
||||
expect(inheritedDescription("OpenAI-Compatible", "inherited")).toBe("OpenAI-Compatible (inherited)")
|
||||
expect(inheritedDescription("configured", "partial")).toBe("configured (partially inherited)")
|
||||
})
|
||||
|
||||
test("does not classify built-in defaults as inherited", () => {
|
||||
expect(indexingInheritance("project", {}, {}, [["vectorStore"]])).toBe("none")
|
||||
expect(indexingInheritance("project", {}, {}, [["searchMinScore"]])).toBe("none")
|
||||
})
|
||||
|
||||
test("reveals the inherited tuning value after clearing a project override", () => {
|
||||
const global: IndexingConfig = { searchMinScore: 0.4, qdrant: { url: "http://global", apiKey: "secret" } }
|
||||
const project: IndexingConfig = { searchMinScore: undefined, qdrant: { url: "http://project", apiKey: undefined } }
|
||||
|
||||
expect(mergeIndexingConfig(global, project)).toMatchObject({
|
||||
searchMinScore: 0.4,
|
||||
qdrant: { url: "http://project", apiKey: "secret" },
|
||||
})
|
||||
expect(indexingInheritance("project", global, project, [["searchMinScore"]])).toBe("inherited")
|
||||
})
|
||||
|
||||
test("isolates global auth config from project indexing values", () => {
|
||||
const project: IndexingConfig = { kilo: { apiKey: "project-key", baseUrl: "https://project.test" } }
|
||||
const inherited: IndexingConfig = { enabled: true }
|
||||
const effective: Config = {
|
||||
provider: { kilo: { options: { apiKey: "provider-key" } } },
|
||||
indexing: project,
|
||||
}
|
||||
const global: Config = { provider: effective.provider, indexing: inherited }
|
||||
|
||||
expect(indexingScopeConfig("global", effective, global, inherited)).toEqual(global)
|
||||
expect(indexingScopeConfig("project", effective, global, project)).toEqual(effective)
|
||||
})
|
||||
|
||||
test("unsets cleared nested values without persisting undefined", () => {
|
||||
expect(
|
||||
indexingPatch(
|
||||
{ qdrant: { url: "http://localhost:6333", apiKey: "secret" }, searchMinScore: 0.4 },
|
||||
{ qdrant: { url: "http://localhost:6333", apiKey: undefined }, searchMinScore: undefined },
|
||||
),
|
||||
).toEqual({
|
||||
indexing: { qdrant: { url: "http://localhost:6333" } },
|
||||
unset: [
|
||||
["indexing", "qdrant", "apiKey"],
|
||||
["indexing", "searchMinScore"],
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test("reacts when the project overlay loads", () => {
|
||||
const [scope, setScope] = createSignal<IndexingScope>("project")
|
||||
const [global, setGlobal] = createSignal<IndexingConfig>({ enabled: true, provider: "openai" })
|
||||
const [project, setProject] = createSignal<IndexingConfig>({})
|
||||
const seen: IndexingConfig[] = []
|
||||
|
||||
const dispose = createRoot((dispose) => {
|
||||
const state = createIndexingDialogState({ scope, global, project, resolve: (config) => config })
|
||||
createEffect(() => seen.push(state.config()))
|
||||
return dispose
|
||||
})
|
||||
|
||||
setProject({ enabled: false, provider: "ollama" })
|
||||
setScope("global")
|
||||
setGlobal({ enabled: false, provider: "gemini" })
|
||||
|
||||
expect(seen).toEqual([
|
||||
{ enabled: true, provider: "openai" },
|
||||
{ enabled: false, provider: "ollama" },
|
||||
{ enabled: true, provider: "openai" },
|
||||
{ enabled: false, provider: "gemini" },
|
||||
])
|
||||
dispose()
|
||||
})
|
||||
|
||||
test("resolves selected-scope values and inheritance", () => {
|
||||
const [scope, setScope] = createSignal<IndexingScope>("project")
|
||||
const [global] = createSignal<IndexingConfig>({
|
||||
enabled: true,
|
||||
provider: "openai",
|
||||
openai: { apiKey: "global" },
|
||||
qdrant: { url: "http://global", apiKey: "global-secret" },
|
||||
})
|
||||
const [project, setProject] = createSignal<IndexingConfig>({ qdrant: { url: "http://project" } })
|
||||
|
||||
const result = createRoot((dispose) => {
|
||||
const state = createIndexingDialogState({ scope, global, project, resolve: (config) => config })
|
||||
return { state, dispose }
|
||||
})
|
||||
|
||||
expect(result.state.enabled()).toBe(true)
|
||||
expect(result.state.inherited([["enabled"]])).toBe("inherited")
|
||||
expect(result.state.config()).toEqual({
|
||||
enabled: true,
|
||||
provider: "openai",
|
||||
openai: { apiKey: "global" },
|
||||
qdrant: { url: "http://project", apiKey: "global-secret" },
|
||||
})
|
||||
|
||||
setProject({ enabled: false })
|
||||
expect(result.state.enabled()).toBe(false)
|
||||
expect(result.state.inherited([["enabled"]])).toBe("none")
|
||||
|
||||
setScope("global")
|
||||
expect(result.state.enabled()).toBe(true)
|
||||
expect(result.state.inherited([["enabled"]])).toBe("none")
|
||||
result.dispose()
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,14 @@
|
||||
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
|
||||
import fs from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { CodeIndexManager } from "@kilocode/kilo-indexing/engine"
|
||||
import { normalizeIndexingStatus } from "@kilocode/kilo-indexing/status"
|
||||
import type { Config } from "../../src/config/config"
|
||||
import { GlobalBus } from "../../src/bus/global"
|
||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
||||
import { WorkspaceContext } from "../../src/control-plane/workspace-context"
|
||||
import { KiloIndexing } from "../../src/kilocode/indexing"
|
||||
import { indexingWarningKey } from "../../src/kilocode/indexing-warning"
|
||||
import { IndexingWorker } from "../../src/kilocode/indexing-worker-client"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
@@ -125,6 +130,47 @@ afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
})
|
||||
|
||||
describe("indexing model catalog", () => {
|
||||
test("ignores a project-scoped Kilo origin", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const global = path.join(dir, "global")
|
||||
const project = path.join(dir, "project")
|
||||
await fs.mkdir(path.join(project, ".kilo"), { recursive: true })
|
||||
await fs.mkdir(global, { recursive: true })
|
||||
await Bun.write(path.join(global, "kilo.jsonc"), "{}")
|
||||
await Bun.write(
|
||||
path.join(project, ".kilo", "kilo.jsonc"),
|
||||
JSON.stringify({ indexing: { kilo: { baseUrl: "http://127.0.0.1:4567" } } }),
|
||||
)
|
||||
return { global, project }
|
||||
},
|
||||
})
|
||||
process.env["KILO_CONFIG_DIR"] = tmp.extra.global
|
||||
const calls: string[] = []
|
||||
globalThis.fetch = (async (input) => {
|
||||
calls.push(String(input))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
defaultModel: "provider/model",
|
||||
models: [{ id: "provider/model", name: "Provider Model", dimension: 1024, scoreThreshold: 0.4 }],
|
||||
aliases: {},
|
||||
}),
|
||||
)
|
||||
}) as typeof fetch
|
||||
|
||||
const response = await Server.Default().app.request("/indexing/models", {
|
||||
headers: { "x-kilo-directory": tmp.extra.project },
|
||||
})
|
||||
|
||||
const catalogs = calls.filter((url) => url.includes("embedding-models"))
|
||||
expect(response.status).toBe(200)
|
||||
expect(catalogs).toHaveLength(1)
|
||||
expect(catalogs[0]).not.toContain("127.0.0.1:4567")
|
||||
})
|
||||
})
|
||||
|
||||
describe("indexing startup degradation", () => {
|
||||
test("keeps server routes alive when indexing initialization fails", async () => {
|
||||
const init = spyOn(CodeIndexManager.prototype, "initialize").mockRejectedValue(error)
|
||||
@@ -161,6 +207,171 @@ describe("indexing startup degradation", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("retains and deduplicates indexing warnings for TUI replay", async () => {
|
||||
const warning = {
|
||||
code: "qdrant.version-incompatible" as const,
|
||||
message:
|
||||
"Client version 1.17.0 is incompatible with server version 1.14.1. Set checkCompatibility=false to skip version check.",
|
||||
}
|
||||
const events: (typeof warning)[] = []
|
||||
const workspaces: (string | undefined)[] = []
|
||||
let emit: IndexingWorker.Hooks["warning"] | undefined
|
||||
|
||||
IndexingWorker.override((_directory, _root, hooks) => {
|
||||
emit = hooks.warning
|
||||
return {
|
||||
async init() {
|
||||
hooks.log({ level: "warn", message: warning.message })
|
||||
hooks.warning(warning)
|
||||
hooks.warning(warning)
|
||||
return {
|
||||
state: "Standby",
|
||||
message: "Indexing paused.",
|
||||
processedFiles: 0,
|
||||
totalFiles: 0,
|
||||
percent: 0,
|
||||
}
|
||||
},
|
||||
async search() {
|
||||
return []
|
||||
},
|
||||
async dispose() {},
|
||||
}
|
||||
})
|
||||
|
||||
await using tmp = await tmpdir({ git: true, config: cfg })
|
||||
process.env["KILO_CONFIG_DIR"] = tmp.path
|
||||
const on = (data: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
payload?: { type?: string; properties?: typeof warning }
|
||||
}) => {
|
||||
if (data.directory !== tmp.path) return
|
||||
if (data.payload?.type !== KiloIndexing.Warning.type) return
|
||||
if (data.payload.properties) events.push(data.payload.properties)
|
||||
workspaces.push(data.workspace)
|
||||
}
|
||||
GlobalBus.on("event", on)
|
||||
|
||||
try {
|
||||
const workspace = WorkspaceID.make("wrk_indexing_warning")
|
||||
await WorkspaceContext.provide({
|
||||
workspaceID: workspace,
|
||||
fn: () =>
|
||||
WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: () => KiloIndexing.current(),
|
||||
}),
|
||||
})
|
||||
const app = Server.Default().app
|
||||
|
||||
const list = await (async () => {
|
||||
for (const _ of Array.from({ length: 100 })) {
|
||||
const response = await app.request("/indexing/warnings", {
|
||||
headers: {
|
||||
"x-kilo-directory": tmp.path,
|
||||
},
|
||||
})
|
||||
expect(response.status).toBe(200)
|
||||
const body = (await response.json()) as (typeof warning)[]
|
||||
if (body.length > 0) return body
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
}
|
||||
throw new Error("indexing warning was not retained")
|
||||
})()
|
||||
for (const _ of Array.from({ length: 100 })) {
|
||||
if (events.length > 0 && events.length === workspaces.length) break
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
}
|
||||
|
||||
expect(list).toEqual([warning])
|
||||
expect(events.length).toBeGreaterThan(0)
|
||||
expect(events.every((item) => indexingWarningKey(item) === indexingWarningKey(warning))).toBe(true)
|
||||
expect(workspaces.every((item) => item === undefined || item === workspace)).toBe(true)
|
||||
|
||||
const offset = events.length
|
||||
const second = WorkspaceID.make("wrk_indexing_warning_second")
|
||||
await WorkspaceContext.provide({
|
||||
workspaceID: second,
|
||||
fn: () =>
|
||||
WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: () => KiloIndexing.warnings(),
|
||||
}),
|
||||
})
|
||||
const next = { ...warning, message: `${warning.message} Again.` }
|
||||
emit?.(next)
|
||||
for (const _ of Array.from({ length: 100 })) {
|
||||
if (workspaces.slice(offset).includes(workspace) && workspaces.slice(offset).includes(second)) break
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
}
|
||||
|
||||
const scoped = workspaces.slice(offset)
|
||||
expect(events.slice(offset).every((item) => indexingWarningKey(item) === indexingWarningKey(next))).toBe(true)
|
||||
expect(scoped.includes(workspace)).toBe(true)
|
||||
expect(scoped.includes(second)).toBe(true)
|
||||
expect(scoped.every((item) => item === undefined || item === workspace || item === second)).toBe(true)
|
||||
} finally {
|
||||
GlobalBus.off("event", on)
|
||||
}
|
||||
})
|
||||
|
||||
test("coalesces burst indexing progress publications", async () => {
|
||||
const complete: KiloIndexing.Status = {
|
||||
state: "Complete",
|
||||
message: "Index up-to-date.",
|
||||
processedFiles: 50,
|
||||
totalFiles: 50,
|
||||
percent: 100,
|
||||
}
|
||||
IndexingWorker.override((_directory, _root, hooks) => ({
|
||||
async init() {
|
||||
for (const processedFiles of Array.from({ length: 50 }, (_, index) => index + 1)) {
|
||||
hooks.status({
|
||||
state: "In Progress",
|
||||
message: `Indexed ${processedFiles} / 50 files.`,
|
||||
processedFiles,
|
||||
totalFiles: 50,
|
||||
percent: processedFiles * 2,
|
||||
})
|
||||
}
|
||||
return complete
|
||||
},
|
||||
async search() {
|
||||
return []
|
||||
},
|
||||
async dispose() {},
|
||||
}))
|
||||
|
||||
await using tmp = await tmpdir({ git: true, config: cfg })
|
||||
process.env["KILO_CONFIG_DIR"] = tmp.path
|
||||
const events: KiloIndexing.Status[] = []
|
||||
const on = (data: {
|
||||
directory?: string
|
||||
payload?: { type?: string; properties?: { status?: KiloIndexing.Status } }
|
||||
}) => {
|
||||
if (data.directory !== tmp.path) return
|
||||
if (data.payload?.type !== KiloIndexing.Event.type) return
|
||||
if (data.payload.properties?.status) events.push(data.payload.properties.status)
|
||||
}
|
||||
GlobalBus.on("event", on)
|
||||
|
||||
try {
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => expect(await wait(() => KiloIndexing.current(), "Complete")).toEqual(complete),
|
||||
})
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
|
||||
const progress = events.filter((status) => status.state === "In Progress")
|
||||
expect(progress.length).toBeLessThanOrEqual(2)
|
||||
expect(progress.filter((status) => status.processedFiles > 0)).toHaveLength(1)
|
||||
expect(events.filter((status) => status.state === "Complete")).toEqual([complete])
|
||||
} finally {
|
||||
GlobalBus.off("event", on)
|
||||
}
|
||||
})
|
||||
|
||||
test("reports routes as in progress while initialization is in flight", async () => {
|
||||
await using tmp = await tmpdir({ git: true, config: cfg })
|
||||
process.env["KILO_CONFIG_DIR"] = tmp.path
|
||||
@@ -419,7 +630,7 @@ describe("indexing startup degradation", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("keeps configured dimensions for supported Kilo models", async () => {
|
||||
test("uses hosted dimensions for supported Kilo models", async () => {
|
||||
global.fetch = (() =>
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
@@ -461,7 +672,7 @@ describe("indexing startup degradation", () => {
|
||||
expect(init.mock.calls[0]?.[0]).toMatchObject({
|
||||
embedderProvider: "kilo",
|
||||
modelId: "openai/text-embedding-3-small",
|
||||
modelDimension: 256,
|
||||
modelDimension: 1536,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -472,7 +683,7 @@ describe("indexing startup degradation", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("does not execute stored Kilo models when the hosted catalog is unavailable", async () => {
|
||||
test("leaves Kilo model metadata unset when the hosted catalog is unavailable", async () => {
|
||||
global.fetch = (() => Promise.resolve(new Response(undefined, { status: 500 }))) as unknown as typeof global.fetch
|
||||
const init = spyOn(CodeIndexManager.prototype, "initialize").mockResolvedValue({ requiresRestart: false })
|
||||
const key = process.env.KILO_API_KEY
|
||||
@@ -486,9 +697,12 @@ describe("indexing startup degradation", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await called(init)
|
||||
expect(init.mock.calls[0]?.[0]).toMatchObject({ embedderProvider: "kilo" })
|
||||
expect(init.mock.calls[0]?.[0].modelId).toBeUndefined()
|
||||
expect(init.mock.calls[0]?.[0].modelDimension).toBeUndefined()
|
||||
expect(init.mock.calls[0]?.[0]).toMatchObject({
|
||||
embedderProvider: "kilo",
|
||||
modelId: undefined,
|
||||
modelDimension: undefined,
|
||||
searchMinScore: undefined,
|
||||
})
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { indexingErrorMessage, indexingWarningKey, parseQdrantWarning } from "../../src/kilocode/indexing-warning"
|
||||
|
||||
describe("parseQdrantWarning", () => {
|
||||
test("classifies incompatible Qdrant versions", () => {
|
||||
const message =
|
||||
"Client version 1.17.0 is incompatible with server version 1.14.1. Major versions should match and minor version difference must not exceed 1. Set checkCompatibility=false to skip version check."
|
||||
|
||||
expect(parseQdrantWarning(message)).toEqual({
|
||||
code: "qdrant.version-incompatible",
|
||||
message:
|
||||
"Client version 1.17.0 is incompatible with server version 1.14.1. Set checkCompatibility=false to skip version check.",
|
||||
})
|
||||
})
|
||||
|
||||
test("classifies unavailable Qdrant versions", () => {
|
||||
const message =
|
||||
"Failed to obtain server version. Unable to check client-server compatibility. Set checkCompatibility=false to skip version check."
|
||||
|
||||
expect(parseQdrantWarning(message)).toEqual({
|
||||
code: "qdrant.version-unavailable",
|
||||
message,
|
||||
})
|
||||
})
|
||||
|
||||
test("ignores unrelated warnings and non-string values", () => {
|
||||
expect(parseQdrantWarning("Api key is used with unsecure connection.")).toBeUndefined()
|
||||
expect(parseQdrantWarning(new Error("warning"))).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
test("classifies error statuses for TUI notifications", () => {
|
||||
const status = { processedFiles: 0, totalFiles: 0, percent: 0 }
|
||||
|
||||
expect(indexingErrorMessage({ ...status, state: "Error", message: "Unable to connect" })).toBe("Unable to connect")
|
||||
expect(indexingErrorMessage({ ...status, state: "Complete", message: "Index up-to-date." })).toBeUndefined()
|
||||
})
|
||||
|
||||
test("indexingWarningKey includes the warning code and message", () => {
|
||||
expect(indexingWarningKey({ code: "qdrant.version-unavailable", message: "warning" })).toBe(
|
||||
"qdrant.version-unavailable\u0000warning",
|
||||
)
|
||||
})
|
||||
@@ -8,6 +8,8 @@ test("runs indexing engine requests in its worker", async () => {
|
||||
const engine = IndexingWorker.create(tmp.path, tmp.path, {
|
||||
status() {},
|
||||
telemetry() {},
|
||||
warning() {},
|
||||
log() {},
|
||||
failure(err) {
|
||||
failures.push(err)
|
||||
},
|
||||
@@ -22,3 +24,59 @@ test("runs indexing engine requests in its worker", async () => {
|
||||
|
||||
expect(failures).toEqual([])
|
||||
})
|
||||
|
||||
test("reuses enabled workers across provider initialization errors", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const drivers = new Set<IndexingWorker.Driver>()
|
||||
|
||||
for (const _ of Array.from({ length: 3 })) {
|
||||
const failures: unknown[] = []
|
||||
const warnings: string[] = []
|
||||
const engine = IndexingWorker.create(tmp.path, tmp.path, {
|
||||
status() {},
|
||||
telemetry() {},
|
||||
warning(item) {
|
||||
warnings.push(item.code)
|
||||
},
|
||||
log() {},
|
||||
failure(err) {
|
||||
failures.push(err)
|
||||
},
|
||||
})
|
||||
drivers.add(engine)
|
||||
|
||||
const err = await engine
|
||||
.init({
|
||||
enabled: true,
|
||||
embedderProvider: "ollama",
|
||||
ollamaBaseUrl: "http://127.0.0.1:1",
|
||||
modelId: "nomic-embed-text",
|
||||
modelDimension: 768,
|
||||
vectorStoreProvider: "qdrant",
|
||||
qdrantUrl: "http://127.0.0.1:1",
|
||||
})
|
||||
.then(
|
||||
() => undefined,
|
||||
(err) => err,
|
||||
)
|
||||
await engine.dispose()
|
||||
|
||||
expect(err).toBeInstanceOf(Error)
|
||||
expect(warnings).toContain("qdrant.version-unavailable")
|
||||
expect(failures).toEqual([])
|
||||
}
|
||||
|
||||
const engine = IndexingWorker.create(tmp.path, tmp.path, {
|
||||
status() {},
|
||||
telemetry() {},
|
||||
warning() {},
|
||||
log() {},
|
||||
failure() {},
|
||||
})
|
||||
const status = await engine.init({ enabled: false, embedderProvider: "openai" })
|
||||
await engine.dispose()
|
||||
|
||||
expect(status.state).toBe("Disabled")
|
||||
expect(drivers.has(engine)).toBe(true)
|
||||
expect(drivers.size).toBe(1)
|
||||
})
|
||||
|
||||
@@ -54,14 +54,14 @@ async function writeConfig(dir: string, config: unknown) {
|
||||
await Filesystem.write(path.join(dir, "kilo.json"), JSON.stringify(config, null, 2))
|
||||
}
|
||||
|
||||
test("project config update creates .kilo/kilo.json and reloads it", async () => {
|
||||
test("project config update creates .kilo/kilo.jsonc and reloads it", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await save({ model: "updated/model" } as any)
|
||||
|
||||
const written = await Filesystem.readJson<{ model: string }>(path.join(tmp.path, ".kilo", "kilo.json"))
|
||||
const written = await Filesystem.readJson<{ model: string }>(path.join(tmp.path, ".kilo", "kilo.jsonc"))
|
||||
expect(written.model).toBe("updated/model")
|
||||
|
||||
const loaded = await load()
|
||||
@@ -77,7 +77,7 @@ test("project config update skips empty delete-only writes when no config exists
|
||||
fn: async () => {
|
||||
await save({ provider: { missing: null } } as any)
|
||||
|
||||
await expect(fs.access(path.join(tmp.path, ".kilo", "kilo.json"))).rejects.toThrow()
|
||||
await expect(fs.access(path.join(tmp.path, ".kilo", "kilo.jsonc"))).rejects.toThrow()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -108,6 +108,95 @@ describe("config overlay routes", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.serial("marks global indexing values inherited in project scope", async () => {
|
||||
await using global = await tmpdir()
|
||||
await using project = await tmpdir()
|
||||
;(Global.Path as { config: string }).config = global.path
|
||||
await config(global.path, {
|
||||
indexing: {
|
||||
enabled: true,
|
||||
provider: "ollama",
|
||||
ollama: { baseUrl: "http://localhost:11434" },
|
||||
},
|
||||
})
|
||||
await invalidate()
|
||||
|
||||
const body = await json<Overlay>(await req(project.path, "/config/overlay?scope=project"))
|
||||
|
||||
expect(body.fields["indexing.enabled"]).toMatchObject({ source: "global", inherited: true, value: true })
|
||||
expect(body.fields["indexing.provider"]).toMatchObject({ source: "global", inherited: true, value: "ollama" })
|
||||
expect(body.fields["indexing.ollama.baseUrl"]).toMatchObject({
|
||||
source: "global",
|
||||
inherited: true,
|
||||
value: "http://localhost:11434",
|
||||
})
|
||||
})
|
||||
|
||||
test.serial("excludes project indexing values from global scope", async () => {
|
||||
await using project = await tmpdir()
|
||||
const global: Config.Info = {
|
||||
indexing: {
|
||||
enabled: true,
|
||||
provider: "openai",
|
||||
openai: { apiKey: "global-secret" },
|
||||
},
|
||||
}
|
||||
const local: Config.Info = {
|
||||
indexing: {
|
||||
enabled: false,
|
||||
provider: "ollama",
|
||||
ollama: { baseUrl: "http://project:11434" },
|
||||
},
|
||||
}
|
||||
await config(project.path, local)
|
||||
|
||||
const body = await KilocodeConfigOverlay.resolve({
|
||||
directory: project.path,
|
||||
scope: "global",
|
||||
effective: local,
|
||||
global,
|
||||
sources: [],
|
||||
})
|
||||
|
||||
expect(body.fields["indexing.enabled"]).toMatchObject({ source: "global", value: true })
|
||||
expect(body.fields["indexing.provider"]).toMatchObject({ source: "global", value: "openai" })
|
||||
expect(body.fields["indexing.openai.apiKey"]).toMatchObject({ source: "global", value: "global-secret" })
|
||||
expect(body.fields["indexing.ollama.baseUrl"]).toMatchObject({ source: "default" })
|
||||
expect(body.fields["indexing.ollama.baseUrl"].value).toBeUndefined()
|
||||
})
|
||||
|
||||
test.serial("writes project indexing overrides to .kilo/kilo.jsonc", async () => {
|
||||
await using global = await tmpdir()
|
||||
await using project = await tmpdir()
|
||||
;(Global.Path as { config: string }).config = global.path
|
||||
await config(global.path, { indexing: { enabled: true, provider: "openai" } })
|
||||
await invalidate()
|
||||
|
||||
await json(
|
||||
await req(project.path, "/config/overlay", {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
scope: "project",
|
||||
set: { indexing: { enabled: false, provider: "ollama", ollama: { baseUrl: "http://127.0.0.1:11434" } } },
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
const file = path.join(project.path, ".kilo", "kilo.jsonc")
|
||||
const saved = (await Bun.file(file).json()) as { indexing: Record<string, unknown> }
|
||||
const body = await json<Overlay>(await req(project.path, "/config/overlay?scope=project"))
|
||||
|
||||
expect(await Bun.file(path.join(project.path, ".kilo", "kilo.json")).exists()).toBe(false)
|
||||
expect(saved.indexing).toEqual({
|
||||
enabled: false,
|
||||
provider: "ollama",
|
||||
ollama: { baseUrl: "http://127.0.0.1:11434" },
|
||||
})
|
||||
expect(body.fields["indexing.enabled"]).toMatchObject({ source: "project", value: false })
|
||||
expect(body.fields["indexing.provider"]).toMatchObject({ source: "project", value: "ollama" })
|
||||
})
|
||||
|
||||
test.serial("removes local scalar override and falls back to global", async () => {
|
||||
await using global = await tmpdir()
|
||||
await using project = await tmpdir({ config: { model: "kilo/project-model", username: "alice" } })
|
||||
@@ -150,7 +239,7 @@ describe("config overlay routes", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
const saved = (await Bun.file(path.join(project.path, ".kilo", "kilo.json")).json()) as {
|
||||
const saved = (await Bun.file(path.join(project.path, ".kilo", "kilo.jsonc")).json()) as {
|
||||
mcp: Record<string, unknown>
|
||||
}
|
||||
expect(Object.keys(saved.mcp)).toEqual(["local"])
|
||||
@@ -173,7 +262,7 @@ describe("config overlay routes", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
const saved = (await Bun.file(path.join(project.path, ".kilo", "kilo.json")).json()) as {
|
||||
const saved = (await Bun.file(path.join(project.path, ".kilo", "kilo.jsonc")).json()) as {
|
||||
mcp: Record<string, unknown>
|
||||
}
|
||||
expect(saved.mcp).toEqual({ shared: { enabled: false } })
|
||||
@@ -254,6 +343,7 @@ describe("config overlay routes", () => {
|
||||
;(Global.Path as { config: string }).config = global.path
|
||||
await config(global.path, { permission: { edit: "ask" } })
|
||||
await invalidate()
|
||||
await disposeAllInstances()
|
||||
const target = app(value)
|
||||
|
||||
const before = await json<Agent[]>(await request(target, project.path, "/agent"))
|
||||
|
||||
@@ -129,6 +129,8 @@ export const kiloScenarios: Scenario[] = [
|
||||
}))
|
||||
.json(200, (body) => check(body === null, "missing worktree diff detail should return null")),
|
||||
http.protected.get("/indexing/status", "indexing.status").json(200, object),
|
||||
http.protected.get("/indexing/models", "indexing.models").json(200, object),
|
||||
http.protected.get("/indexing/warnings", "indexing.warnings").json(200, array),
|
||||
http.protected.get("/kilo/profile", "kilo.profile").probe({ path: "/path" }).status(401),
|
||||
http.protected.get("/kilo/modes", "kilo.modes").json(200, (body) => {
|
||||
object(body)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Result, Schema as EffectSchema } from "effect"
|
||||
import { OpenApi } from "effect/unstable/httpapi"
|
||||
import { AgentBuilderPaths } from "../../../src/kilocode/server/httpapi/groups/agent-builder"
|
||||
import { BackgroundProcessPaths } from "../../../src/kilocode/server/httpapi/groups/background-process"
|
||||
import { ConfigConsolePaths } from "../../../src/kilocode/server/httpapi/groups/config-console"
|
||||
import { IndexingPaths } from "../../../src/kilocode/server/httpapi/groups/indexing"
|
||||
import { IndexingPaths, KiloEmbeddingModel } from "../../../src/kilocode/server/httpapi/groups/indexing"
|
||||
import { KiloGatewayPaths } from "../../../src/kilocode/server/httpapi/groups/kilo-gateway"
|
||||
import { NetworkPaths } from "../../../src/kilocode/server/httpapi/groups/network"
|
||||
import { TelemetryPaths } from "../../../src/kilocode/server/httpapi/groups/telemetry"
|
||||
@@ -40,6 +41,25 @@ describe("Kilo PublicApi OpenAPI contract", () => {
|
||||
expect(spec.info.description).toBe("kilo api")
|
||||
})
|
||||
|
||||
test("constrains embedding model metadata", () => {
|
||||
const accepts = (dimension: number, scoreThreshold: number) =>
|
||||
Result.isSuccess(
|
||||
EffectSchema.decodeUnknownResult(KiloEmbeddingModel)({
|
||||
id: "provider/model",
|
||||
name: "Model",
|
||||
dimension,
|
||||
scoreThreshold,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(accepts(1, 0)).toBe(true)
|
||||
expect(accepts(1024, 1)).toBe(true)
|
||||
expect(accepts(0, 0.5)).toBe(false)
|
||||
expect(accepts(1.5, 0.5)).toBe(false)
|
||||
expect(accepts(1024, -0.1)).toBe(false)
|
||||
expect(accepts(1024, 1.1)).toBe(false)
|
||||
})
|
||||
|
||||
test("constrains agent builder route ids", () => {
|
||||
const spec = OpenApi.fromApi(PublicApi)
|
||||
const save = AgentBuilderPaths.save.replace(":id", "{id}")
|
||||
@@ -83,6 +103,7 @@ describe("Kilo PublicApi OpenAPI contract", () => {
|
||||
{ method: "get", path: ConfigConsolePaths.overlay },
|
||||
{ method: "patch", path: ConfigConsolePaths.overlay },
|
||||
{ method: "get", path: IndexingPaths.status },
|
||||
{ method: "get", path: IndexingPaths.models },
|
||||
] satisfies Array<{ method: Method; path: string }>
|
||||
|
||||
for (const route of routes) {
|
||||
|
||||
@@ -85,7 +85,9 @@ import type {
|
||||
GlobalHealthResponses,
|
||||
GlobalUpgradeErrors,
|
||||
GlobalUpgradeResponses,
|
||||
IndexingModelsResponses,
|
||||
IndexingStatusResponses,
|
||||
IndexingWarningsResponses,
|
||||
InstanceDisposeResponses,
|
||||
KiloAudioTranscriptionsErrors,
|
||||
KiloAudioTranscriptionsResponses,
|
||||
@@ -6187,6 +6189,66 @@ export class Indexing extends HeyApiClient {
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get indexing warnings
|
||||
*
|
||||
* Retrieve code indexing warnings for the active project.
|
||||
*/
|
||||
public warnings<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "query", key: "directory" },
|
||||
{ in: "query", key: "workspace" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).get<IndexingWarningsResponses, unknown, ThrowOnError>({
|
||||
url: "/indexing/warnings",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* List Kilo embedding models
|
||||
*
|
||||
* Retrieve the embedding models available through the active Kilo account.
|
||||
*/
|
||||
public models<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "query", key: "directory" },
|
||||
{ in: "query", key: "workspace" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).get<IndexingModelsResponses, unknown, ThrowOnError>({
|
||||
url: "/indexing/models",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Audio extends HeyApiClient {
|
||||
|
||||
@@ -5,10 +5,17 @@ export type ClientOptions = {
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| EventServerInstanceDisposed
|
||||
| EventServerConnected
|
||||
| EventGlobalDisposed
|
||||
| EventGlobalConfigUpdated
|
||||
| EventTuiPromptAppend
|
||||
| EventTuiCommandExecute
|
||||
| EventTuiToastShow1
|
||||
| EventTuiSessionSelect
|
||||
| EventKilocodeAgentManagerStart
|
||||
| EventIndexingStatus
|
||||
| EventIndexingWarning
|
||||
| EventServerInstanceDisposed
|
||||
| EventFileEdited
|
||||
| EventFileWatcherUpdated
|
||||
| EventQuestionAsked
|
||||
@@ -16,10 +23,6 @@ export type Event =
|
||||
| EventQuestionRejected
|
||||
| EventLspClientDiagnostics
|
||||
| EventLspUpdated
|
||||
| EventTuiPromptAppend
|
||||
| EventTuiCommandExecute
|
||||
| EventTuiToastShow1
|
||||
| EventTuiSessionSelect
|
||||
| EventMcpToolsChanged
|
||||
| EventMcpBrowserOpenFailed
|
||||
| EventSessionNetworkAsked
|
||||
@@ -44,7 +47,6 @@ export type Event =
|
||||
| EventSessionCompacted
|
||||
| EventCommandExecuted
|
||||
| EventProjectUpdated
|
||||
| EventKilocodeAgentManagerStart
|
||||
| EventVcsBranchUpdated
|
||||
| EventKiloSessionsRemoteStatusChanged
|
||||
| EventWorkspaceReady
|
||||
@@ -58,7 +60,6 @@ export type Event =
|
||||
| EventPtyDeleted
|
||||
| EventInstallationUpdated
|
||||
| EventInstallationUpdateAvailable
|
||||
| EventIndexingStatus
|
||||
|
||||
export type OAuth = {
|
||||
type: "oauth"
|
||||
@@ -85,6 +86,76 @@ export type WellKnownAuth = {
|
||||
|
||||
export type Auth = OAuth | ApiAuth | WellKnownAuth
|
||||
|
||||
export type EventTuiPromptAppend = {
|
||||
id: string
|
||||
type: "tui.prompt.append"
|
||||
properties: {
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiCommandExecute = {
|
||||
id: string
|
||||
type: "tui.command.execute"
|
||||
properties: {
|
||||
command:
|
||||
| "session.list"
|
||||
| "session.new"
|
||||
| "session.share"
|
||||
| "session.interrupt"
|
||||
| "session.compact"
|
||||
| "session.page.up"
|
||||
| "session.page.down"
|
||||
| "session.line.up"
|
||||
| "session.line.down"
|
||||
| "session.half.page.up"
|
||||
| "session.half.page.down"
|
||||
| "session.first"
|
||||
| "session.last"
|
||||
| "prompt.clear"
|
||||
| "prompt.submit"
|
||||
| "agent.cycle"
|
||||
| string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiToastShow = {
|
||||
id: string
|
||||
type: "tui.toast.show"
|
||||
properties: {
|
||||
title?: string
|
||||
message: string
|
||||
variant: "info" | "success" | "warning" | "error"
|
||||
duration?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiSessionSelect = {
|
||||
id: string
|
||||
type: "tui.session.select"
|
||||
properties: {
|
||||
/**
|
||||
* Session ID to navigate to
|
||||
*/
|
||||
sessionID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type IndexingStatusState = "Disabled" | "In Progress" | "Complete" | "Error" | "Standby"
|
||||
|
||||
export type IndexingStatus = {
|
||||
state: IndexingStatusState
|
||||
message: string
|
||||
processedFiles: number
|
||||
totalFiles: number
|
||||
percent: number
|
||||
}
|
||||
|
||||
export type IndexingWarning = {
|
||||
code: "qdrant.version-incompatible" | "qdrant.version-unavailable"
|
||||
message: string
|
||||
}
|
||||
|
||||
export type QuestionOption = {
|
||||
/**
|
||||
* Display text (1-5 words, concise)
|
||||
@@ -147,61 +218,6 @@ export type QuestionRejected = {
|
||||
requestID: string
|
||||
}
|
||||
|
||||
export type EventTuiPromptAppend = {
|
||||
id: string
|
||||
type: "tui.prompt.append"
|
||||
properties: {
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiCommandExecute = {
|
||||
id: string
|
||||
type: "tui.command.execute"
|
||||
properties: {
|
||||
command:
|
||||
| "session.list"
|
||||
| "session.new"
|
||||
| "session.share"
|
||||
| "session.interrupt"
|
||||
| "session.compact"
|
||||
| "session.page.up"
|
||||
| "session.page.down"
|
||||
| "session.line.up"
|
||||
| "session.line.down"
|
||||
| "session.half.page.up"
|
||||
| "session.half.page.down"
|
||||
| "session.first"
|
||||
| "session.last"
|
||||
| "prompt.clear"
|
||||
| "prompt.submit"
|
||||
| "agent.cycle"
|
||||
| string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiToastShow = {
|
||||
id: string
|
||||
type: "tui.toast.show"
|
||||
properties: {
|
||||
title?: string
|
||||
message: string
|
||||
variant: "info" | "success" | "warning" | "error"
|
||||
duration?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiSessionSelect = {
|
||||
id: string
|
||||
type: "tui.session.select"
|
||||
properties: {
|
||||
/**
|
||||
* Session ID to navigate to
|
||||
*/
|
||||
sessionID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionNetworkWait = {
|
||||
id: string
|
||||
sessionID: string
|
||||
@@ -416,16 +432,6 @@ export type Pty = {
|
||||
sessionID?: string | null
|
||||
}
|
||||
|
||||
export type IndexingStatusState = "Disabled" | "In Progress" | "Complete" | "Error" | "Standby"
|
||||
|
||||
export type IndexingStatus = {
|
||||
state: IndexingStatusState
|
||||
message: string
|
||||
processedFiles: number
|
||||
totalFiles: number
|
||||
percent: number
|
||||
}
|
||||
|
||||
export type OutputFormatText = {
|
||||
type: "text"
|
||||
}
|
||||
@@ -859,10 +865,17 @@ export type GlobalEvent = {
|
||||
project?: string
|
||||
workspace?: string
|
||||
payload:
|
||||
| EventServerInstanceDisposed
|
||||
| EventServerConnected
|
||||
| EventGlobalDisposed
|
||||
| EventGlobalConfigUpdated
|
||||
| EventTuiPromptAppend
|
||||
| EventTuiCommandExecute
|
||||
| EventTuiToastShow
|
||||
| EventTuiSessionSelect
|
||||
| EventKilocodeAgentManagerStart
|
||||
| EventIndexingStatus
|
||||
| EventIndexingWarning
|
||||
| EventServerInstanceDisposed
|
||||
| EventFileEdited
|
||||
| EventFileWatcherUpdated
|
||||
| EventQuestionAsked
|
||||
@@ -870,10 +883,6 @@ export type GlobalEvent = {
|
||||
| EventQuestionRejected
|
||||
| EventLspClientDiagnostics
|
||||
| EventLspUpdated
|
||||
| EventTuiPromptAppend
|
||||
| EventTuiCommandExecute
|
||||
| EventTuiToastShow
|
||||
| EventTuiSessionSelect
|
||||
| EventMcpToolsChanged
|
||||
| EventMcpBrowserOpenFailed
|
||||
| EventSessionNetworkAsked
|
||||
@@ -898,7 +907,6 @@ export type GlobalEvent = {
|
||||
| EventSessionCompacted
|
||||
| EventCommandExecuted
|
||||
| EventProjectUpdated
|
||||
| EventKilocodeAgentManagerStart
|
||||
| EventVcsBranchUpdated
|
||||
| EventKiloSessionsRemoteStatusChanged
|
||||
| EventWorkspaceReady
|
||||
@@ -912,7 +920,6 @@ export type GlobalEvent = {
|
||||
| EventPtyDeleted
|
||||
| EventInstallationUpdated
|
||||
| EventInstallationUpdateAvailable
|
||||
| EventIndexingStatus
|
||||
| SyncEventMessageUpdated
|
||||
| SyncEventMessageRemoved
|
||||
| SyncEventMessagePartUpdated
|
||||
@@ -2177,6 +2184,20 @@ export type TuiKeybindListResponse = {
|
||||
keybinds: Array<TuiKeybindInfo>
|
||||
}
|
||||
|
||||
export type KiloEmbeddingModelCatalog = {
|
||||
defaultModel: string
|
||||
models: Array<{
|
||||
id: string
|
||||
name: string
|
||||
dimension: number
|
||||
scoreThreshold: number
|
||||
note?: string
|
||||
}>
|
||||
aliases: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EffectHttpApiErrorUnauthorized = {
|
||||
_tag: "Unauthorized"
|
||||
}
|
||||
@@ -2723,14 +2744,6 @@ export type SyncEventSessionNextCompactionEnded = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerInstanceDisposed = {
|
||||
id: string
|
||||
type: "server.instance.disposed"
|
||||
properties: {
|
||||
directory: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerConnected = {
|
||||
id: string
|
||||
type: "server.connected"
|
||||
@@ -2755,6 +2768,44 @@ export type EventGlobalConfigUpdated = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventKilocodeAgentManagerStart = {
|
||||
id: string
|
||||
type: "kilocode.agent_manager.start"
|
||||
properties: {
|
||||
requestID: string
|
||||
sessionID: string
|
||||
mode: "worktree" | "local"
|
||||
versions?: boolean
|
||||
tasks: Array<{
|
||||
prompt?: string
|
||||
name?: string
|
||||
branchName?: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export type EventIndexingStatus = {
|
||||
id: string
|
||||
type: "indexing.status"
|
||||
properties: {
|
||||
status: IndexingStatus
|
||||
}
|
||||
}
|
||||
|
||||
export type EventIndexingWarning = {
|
||||
id: string
|
||||
type: "indexing.warning"
|
||||
properties: IndexingWarning
|
||||
}
|
||||
|
||||
export type EventServerInstanceDisposed = {
|
||||
id: string
|
||||
type: "server.instance.disposed"
|
||||
properties: {
|
||||
directory: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventFileEdited = {
|
||||
id: string
|
||||
type: "file.edited"
|
||||
@@ -3032,22 +3083,6 @@ export type EventProjectUpdated = {
|
||||
properties: Project
|
||||
}
|
||||
|
||||
export type EventKilocodeAgentManagerStart = {
|
||||
id: string
|
||||
type: "kilocode.agent_manager.start"
|
||||
properties: {
|
||||
requestID: string
|
||||
sessionID: string
|
||||
mode: "worktree" | "local"
|
||||
versions?: boolean
|
||||
tasks: Array<{
|
||||
prompt?: string
|
||||
name?: string
|
||||
branchName?: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export type EventVcsBranchUpdated = {
|
||||
id: string
|
||||
type: "vcs.branch.updated"
|
||||
@@ -3156,14 +3191,6 @@ export type EventInstallationUpdateAvailable = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventIndexingStatus = {
|
||||
id: string
|
||||
type: "indexing.status"
|
||||
properties: {
|
||||
status: IndexingStatus
|
||||
}
|
||||
}
|
||||
|
||||
export type PromptSource = {
|
||||
start: number
|
||||
end: number
|
||||
@@ -8153,6 +8180,44 @@ export type IndexingStatusResponses = {
|
||||
|
||||
export type IndexingStatusResponse = IndexingStatusResponses[keyof IndexingStatusResponses]
|
||||
|
||||
export type IndexingWarningsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/indexing/warnings"
|
||||
}
|
||||
|
||||
export type IndexingWarningsResponses = {
|
||||
/**
|
||||
* Indexing warnings
|
||||
*/
|
||||
200: Array<IndexingWarning>
|
||||
}
|
||||
|
||||
export type IndexingWarningsResponse = IndexingWarningsResponses[keyof IndexingWarningsResponses]
|
||||
|
||||
export type IndexingModelsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/indexing/models"
|
||||
}
|
||||
|
||||
export type IndexingModelsResponses = {
|
||||
/**
|
||||
* Kilo embedding model catalog
|
||||
*/
|
||||
200: KiloEmbeddingModelCatalog
|
||||
}
|
||||
|
||||
export type IndexingModelsResponse = IndexingModelsResponses[keyof IndexingModelsResponses]
|
||||
|
||||
export type KiloProfileData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
||||
+482
-309
@@ -11163,6 +11163,98 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/indexing/warnings": {
|
||||
"get": {
|
||||
"tags": ["indexing"],
|
||||
"operationId": "indexing.warnings",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "directory",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "workspace",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Indexing warnings",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/IndexingWarning"
|
||||
},
|
||||
"description": "Indexing warnings"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Retrieve code indexing warnings for the active project.",
|
||||
"summary": "Get indexing warnings",
|
||||
"x-codeSamples": [
|
||||
{
|
||||
"lang": "js",
|
||||
"source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.indexing.warnings({\n ...\n})"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/indexing/models": {
|
||||
"get": {
|
||||
"tags": ["indexing"],
|
||||
"operationId": "indexing.models",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "directory",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "workspace",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Kilo embedding model catalog",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/KiloEmbeddingModelCatalog"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Retrieve the embedding models available through the active Kilo account.",
|
||||
"summary": "List Kilo embedding models",
|
||||
"x-codeSamples": [
|
||||
{
|
||||
"lang": "js",
|
||||
"source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.indexing.models({\n ...\n})"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/kilo/profile": {
|
||||
"get": {
|
||||
"tags": ["kilo"],
|
||||
@@ -14344,9 +14436,6 @@
|
||||
"schemas": {
|
||||
"Event": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerInstanceDisposed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerConnected"
|
||||
},
|
||||
@@ -14356,6 +14445,30 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventGlobalConfigUpdated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.command.execute"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventTuiToastShow1"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.session.select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventKilocodeAgent_managerStart"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventIndexingStatus"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventIndexingWarning"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerInstanceDisposed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventFileEdited"
|
||||
},
|
||||
@@ -14377,18 +14490,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventLspUpdated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.command.execute"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventTuiToastShow1"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.session.select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventMcpToolsChanged"
|
||||
},
|
||||
@@ -14461,9 +14562,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventProjectUpdated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventKilocodeAgent_managerStart"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventVcsBranchUpdated"
|
||||
},
|
||||
@@ -14502,9 +14600,6 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventInstallationUpdate-available"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventIndexingStatus"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -14585,6 +14680,184 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Event.tui.prompt.append": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.prompt.append"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["text"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.command.execute": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.command.execute"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"session.list",
|
||||
"session.new",
|
||||
"session.share",
|
||||
"session.interrupt",
|
||||
"session.compact",
|
||||
"session.page.up",
|
||||
"session.page.down",
|
||||
"session.line.up",
|
||||
"session.line.down",
|
||||
"session.half.page.up",
|
||||
"session.half.page.down",
|
||||
"session.first",
|
||||
"session.last",
|
||||
"prompt.clear",
|
||||
"prompt.submit",
|
||||
"agent.cycle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["command"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.toast.show": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.toast.show"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string",
|
||||
"enum": ["info", "success", "warning", "error"]
|
||||
},
|
||||
"duration": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["message", "variant"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.session.select": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.session.select"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses",
|
||||
"description": "Session ID to navigate to"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"IndexingStatusState": {
|
||||
"type": "string",
|
||||
"enum": ["Disabled", "In Progress", "Complete", "Error", "Standby"]
|
||||
},
|
||||
"IndexingStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/IndexingStatusState"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"processedFiles": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"totalFiles": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"percent": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
}
|
||||
},
|
||||
"required": ["state", "message", "processedFiles", "totalFiles", "percent"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"IndexingWarning": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"enum": ["qdrant.version-incompatible", "qdrant.version-unavailable"]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["code", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"QuestionOption": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -14727,140 +15000,6 @@
|
||||
"required": ["sessionID", "requestID"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.prompt.append": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.prompt.append"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["text"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.command.execute": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.command.execute"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"session.list",
|
||||
"session.new",
|
||||
"session.share",
|
||||
"session.interrupt",
|
||||
"session.compact",
|
||||
"session.page.up",
|
||||
"session.page.down",
|
||||
"session.line.up",
|
||||
"session.line.down",
|
||||
"session.half.page.up",
|
||||
"session.half.page.down",
|
||||
"session.first",
|
||||
"session.last",
|
||||
"prompt.clear",
|
||||
"prompt.submit",
|
||||
"agent.cycle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["command"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.toast.show": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.toast.show"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string",
|
||||
"enum": ["info", "success", "warning", "error"]
|
||||
},
|
||||
"duration": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["message", "variant"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.session.select": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.session.select"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses",
|
||||
"description": "Session ID to navigate to"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SessionNetworkWait": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15490,36 +15629,6 @@
|
||||
"required": ["id", "title", "command", "args", "cwd", "status", "pid"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"IndexingStatusState": {
|
||||
"type": "string",
|
||||
"enum": ["Disabled", "In Progress", "Complete", "Error", "Standby"]
|
||||
},
|
||||
"IndexingStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/IndexingStatusState"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"processedFiles": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"totalFiles": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"percent": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
}
|
||||
},
|
||||
"required": ["state", "message", "processedFiles", "totalFiles", "percent"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"OutputFormatText": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -16871,9 +16980,6 @@
|
||||
},
|
||||
"payload": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerInstanceDisposed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerConnected"
|
||||
},
|
||||
@@ -16883,6 +16989,30 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventGlobalConfigUpdated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.command.execute"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.toast.show"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.session.select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventKilocodeAgent_managerStart"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventIndexingStatus"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventIndexingWarning"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerInstanceDisposed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventFileEdited"
|
||||
},
|
||||
@@ -16904,18 +17034,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventLspUpdated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.command.execute"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.toast.show"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.session.select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventMcpToolsChanged"
|
||||
},
|
||||
@@ -16988,9 +17106,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventProjectUpdated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventKilocodeAgent_managerStart"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventVcsBranchUpdated"
|
||||
},
|
||||
@@ -17030,9 +17145,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventInstallationUpdate-available"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventIndexingStatus"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SyncEventMessageUpdated"
|
||||
},
|
||||
@@ -20744,6 +20856,50 @@
|
||||
"required": ["keybinds"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"KiloEmbeddingModelCatalog": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"defaultModel": {
|
||||
"type": "string"
|
||||
},
|
||||
"models": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"dimension": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"scoreThreshold": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"note": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["id", "name", "dimension", "scoreThreshold"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"aliases": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["defaultModel", "models", "aliases"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"effect_HttpApiError_Unauthorized": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -22636,30 +22792,6 @@
|
||||
"required": ["type", "name", "id", "seq", "aggregateID", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventServerInstanceDisposed": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["server.instance.disposed"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["directory"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventServerConnected": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -22714,6 +22846,126 @@
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventKilocodeAgent_managerStart": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["kilocode.agent_manager.start"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"requestID": {
|
||||
"type": "string"
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["worktree", "local"]
|
||||
},
|
||||
"versions": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"branchName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 20
|
||||
}
|
||||
},
|
||||
"required": ["requestID", "sessionID", "mode", "tasks"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventIndexingStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["indexing.status"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/IndexingStatus"
|
||||
}
|
||||
},
|
||||
"required": ["status"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventIndexingWarning": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["indexing.warning"]
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/components/schemas/IndexingWarning"
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventServerInstanceDisposed": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["server.instance.disposed"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["directory"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventFileEdited": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -23553,61 +23805,6 @@
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventKilocodeAgent_managerStart": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["kilocode.agent_manager.start"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"requestID": {
|
||||
"type": "string"
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["worktree", "local"]
|
||||
},
|
||||
"versions": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"branchName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 20
|
||||
}
|
||||
},
|
||||
"required": ["requestID", "sessionID", "mode", "tasks"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventVcsBranchUpdated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -23936,30 +24133,6 @@
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventIndexingStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["indexing.status"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/IndexingStatus"
|
||||
}
|
||||
},
|
||||
"required": ["status"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PromptSource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user