mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
Make codebase indexing setting project-specific (#9855)
* fix: make indexing enablement project-specific * chore: annotate indexing config changes * fix: save TUI indexing toggle per project * fix: move indexing scope logic to Kilo config
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Respect project-specific semantic indexing decisions instead of enabling indexing globally across workspaces.
|
||||
@@ -2334,7 +2334,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
if (refreshProviders) await this.fetchAndSendProviders()
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: Config write succeeded but post-write refresh failed:", error)
|
||||
const patch = { ...partial, ...project }
|
||||
const patch =
|
||||
partial.indexing === undefined && project.indexing === undefined
|
||||
? { ...partial, ...project }
|
||||
: { ...partial, ...project, indexing: { ...(partial.indexing ?? {}), ...(project.indexing ?? {}) } }
|
||||
const cached = (this.cachedConfigMessage as { config?: unknown } | null)?.config
|
||||
const features = (this.cachedConfigMessage as { features?: unknown } | null)?.features
|
||||
const optimistic =
|
||||
@@ -2348,7 +2351,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
this.pending--
|
||||
}
|
||||
}
|
||||
|
||||
private postConfigFailure(error: unknown): void {
|
||||
console.error("[Kilo New] KiloProvider: Failed to update config:", error)
|
||||
this.postMessage({
|
||||
@@ -2357,7 +2359,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
details: getConfigErrorDetails(error),
|
||||
})
|
||||
}
|
||||
|
||||
private async resolveSession(sessionID?: string, draftID?: string, context?: string) {
|
||||
if (!this.client) return undefined
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { splitConfigByScope } from "../../webview-ui/src/utils/config-scope"
|
||||
|
||||
describe("splitConfigByScope", () => {
|
||||
it("writes indexing enablement to project config only", () => {
|
||||
const split = splitConfigByScope({
|
||||
indexing: {
|
||||
enabled: true,
|
||||
provider: "ollama",
|
||||
},
|
||||
})
|
||||
|
||||
expect(split.global).toEqual({ indexing: { provider: "ollama" } })
|
||||
expect(split.project).toEqual({ indexing: { enabled: true } })
|
||||
})
|
||||
})
|
||||
@@ -13,21 +13,7 @@ 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"
|
||||
|
||||
// Top-level config keys that persist to the project's kilo.json rather than the
|
||||
// global one. Settings that are inherently per-repository (e.g. commit message
|
||||
// conventions) belong here so they don't leak across workspaces.
|
||||
const PROJECT_SCOPED_KEYS: ReadonlySet<string> = new Set(["commit_message"])
|
||||
|
||||
function splitByScope(draft: Partial<Config>) {
|
||||
const global: Record<string, unknown> = {}
|
||||
const project: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(draft)) {
|
||||
if (PROJECT_SCOPED_KEYS.has(key)) project[key] = value
|
||||
else global[key] = value
|
||||
}
|
||||
return { global: global as Partial<Config>, project: project as Partial<Config> }
|
||||
}
|
||||
import { splitConfigByScope } from "../utils/config-scope"
|
||||
|
||||
export interface SaveError {
|
||||
message: string
|
||||
@@ -155,7 +141,7 @@ export const ConfigProvider: ParentComponent = (props) => {
|
||||
// Split so per-project settings (e.g. commit_message.prompt) land in the
|
||||
// workspace's kilo.json instead of the global one. Send one message so the
|
||||
// extension confirms only after both scopes are saved.
|
||||
const split = splitByScope(changes)
|
||||
const split = splitConfigByScope(changes)
|
||||
vscode.postMessage({ type: "updateConfig", config: split.global, projectConfig: split.project })
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Config } from "../types/messages"
|
||||
|
||||
// Top-level config keys that persist to the project's kilo.json rather than the
|
||||
// global one. Settings that are inherently per-repository (e.g. commit message
|
||||
// conventions) belong here so they don't leak across workspaces.
|
||||
const PROJECT_SCOPED_KEYS: ReadonlySet<string> = new Set(["commit_message"])
|
||||
const PROJECT_INDEXING_KEYS: ReadonlySet<string> = new Set(["enabled"])
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function splitIndexing(value: unknown) {
|
||||
if (!isRecord(value)) return { global: value, project: undefined }
|
||||
const global = Object.fromEntries(Object.entries(value).filter(([key]) => !PROJECT_INDEXING_KEYS.has(key)))
|
||||
const project = Object.fromEntries(Object.entries(value).filter(([key]) => PROJECT_INDEXING_KEYS.has(key)))
|
||||
return {
|
||||
global: Object.keys(global).length > 0 ? global : undefined,
|
||||
project: Object.keys(project).length > 0 ? project : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function splitConfigByScope(draft: Partial<Config>) {
|
||||
const global: Record<string, unknown> = {}
|
||||
const project: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(draft)) {
|
||||
if (key === "indexing") {
|
||||
const scoped = splitIndexing(value)
|
||||
if (scoped.global !== undefined) global[key] = scoped.global
|
||||
if (scoped.project !== undefined) project[key] = scoped.project
|
||||
continue
|
||||
}
|
||||
if (PROJECT_SCOPED_KEYS.has(key)) project[key] = value
|
||||
else global[key] = value
|
||||
}
|
||||
return { global: global as Partial<Config>, project: project as Partial<Config> }
|
||||
}
|
||||
@@ -600,10 +600,14 @@ export const layer = Layer.effect(
|
||||
result.plugin_origins = plugins
|
||||
})
|
||||
|
||||
const merge = (source: string, next: Info, kind?: ConfigPlugin.Scope) => {
|
||||
result = mergeConfigConcatArrays(result, next)
|
||||
return mergePluginOrigins(source, next.plugin, kind)
|
||||
}
|
||||
// kilocode_change start
|
||||
const merge = Effect.fnUntraced(function* (source: string, next: Info, kind?: ConfigPlugin.Scope) {
|
||||
const scope = kind ?? (yield* pluginScopeForSource(source))
|
||||
const scoped = KilocodeConfig.scopeIndexing(next, scope)
|
||||
result = mergeConfigConcatArrays(result, scoped)
|
||||
return yield* mergePluginOrigins(source, scoped.plugin, scope)
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
for (const [key, value] of Object.entries(auth)) {
|
||||
if (value.type === "wellknown") {
|
||||
@@ -836,16 +840,19 @@ export const layer = Layer.effect(
|
||||
// kilocode_change end
|
||||
|
||||
// macOS managed preferences (.mobileconfig deployed via MDM) override everything
|
||||
// kilocode_change start
|
||||
const managed = yield* Effect.promise(() => ConfigManaged.readManagedPreferences())
|
||||
if (managed) {
|
||||
result = mergeConfigConcatArrays(
|
||||
result,
|
||||
yield* merge(
|
||||
managed.source,
|
||||
yield* loadConfig(managed.text, {
|
||||
dir: path.dirname(managed.source),
|
||||
source: managed.source,
|
||||
}),
|
||||
"global",
|
||||
)
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
for (const [name, mode] of Object.entries(result.mode ?? {})) {
|
||||
result.agent = mergeDeep(result.agent ?? {}, {
|
||||
|
||||
@@ -76,8 +76,15 @@ async function saveIndexing(
|
||||
indexing: IndexingConfig,
|
||||
toast: ReturnType<typeof useToast>,
|
||||
): Promise<boolean> {
|
||||
const response = await sdk.client.global.config.update({ config: { indexing } })
|
||||
if (response.error) {
|
||||
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 } } })]
|
||||
: []),
|
||||
])
|
||||
if (responses.some((response) => response.error)) {
|
||||
toast.show({ message: "Failed to save indexing config", variant: "error" })
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -109,6 +109,21 @@ export namespace KilocodeConfig {
|
||||
yield* input.fs.writeWithDirs(file, JSON.stringify(merged, null, 2)).pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
export function scopeIndexing(info: Config.Info, scope: "global" | "local"): Config.Info {
|
||||
if (scope !== "global") return info
|
||||
return stripGlobalIndexing(info)
|
||||
}
|
||||
|
||||
function stripGlobalIndexing(info: Config.Info): Config.Info {
|
||||
// Indexing provider/storage settings can be global, but enablement is a per-project decision.
|
||||
if (info.indexing?.enabled === undefined) return info
|
||||
const indexing = Object.fromEntries(Object.entries(info.indexing).filter(([key]) => key !== "enabled"))
|
||||
if (Object.keys(indexing).length > 0) return { ...info, indexing }
|
||||
const copy = { ...info }
|
||||
delete copy.indexing
|
||||
return copy
|
||||
}
|
||||
|
||||
// ── Warning helpers ──────────────────────────────────────────────────
|
||||
|
||||
/** Convert known config-loading error types into a Warning. Returns undefined for unknown errors. */
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// kilocode_change - new file
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Effect, Layer, Option } from "effect"
|
||||
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
||||
import path from "path"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { toIndexingConfigInput } from "@kilocode/kilo-indexing/config"
|
||||
import { Account } from "../../../src/account/account"
|
||||
import { Auth } from "../../../src/auth"
|
||||
import { Config } from "../../../src/config/config"
|
||||
import { Env } from "../../../src/env"
|
||||
import { Instance } from "../../../src/project/instance"
|
||||
import { Filesystem } from "../../../src/util/filesystem"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
|
||||
const infra = CrossSpawnSpawner.defaultLayer.pipe(
|
||||
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
|
||||
)
|
||||
const emptyAccount = Layer.mock(Account.Service)({
|
||||
active: () => Effect.succeed(Option.none()),
|
||||
activeOrg: () => Effect.succeed(Option.none()),
|
||||
})
|
||||
const emptyAuth = Layer.mock(Auth.Service)({
|
||||
all: () => Effect.succeed({}),
|
||||
})
|
||||
const noopNpm = Layer.mock(Npm.Service)({
|
||||
install: () => Effect.void,
|
||||
add: () => Effect.die("not implemented"),
|
||||
which: () => Effect.succeed(Option.none()),
|
||||
})
|
||||
const layer = Config.layer.pipe(
|
||||
Layer.provide(EffectFlock.defaultLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provide(emptyAuth),
|
||||
Layer.provide(emptyAccount),
|
||||
Layer.provideMerge(infra),
|
||||
Layer.provide(noopNpm),
|
||||
)
|
||||
|
||||
const load = () => Effect.runPromise(Config.Service.use((svc) => svc.get()).pipe(Effect.scoped, Effect.provide(layer)))
|
||||
const clear = (wait = false) =>
|
||||
Effect.runPromise(Config.Service.use((svc) => svc.invalidate(wait)).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))
|
||||
}
|
||||
|
||||
describe("kilocode indexing config", () => {
|
||||
afterEach(async () => {
|
||||
await Instance.disposeAll()
|
||||
await clear(true)
|
||||
})
|
||||
|
||||
test("does not inherit global indexing enabled into project config", async () => {
|
||||
await using globalTmp = await tmpdir()
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
const prev = Global.Path.config
|
||||
;(Global.Path as { config: string }).config = globalTmp.path
|
||||
await clear(true)
|
||||
|
||||
try {
|
||||
await writeConfig(globalTmp.path, {
|
||||
$schema: "https://app.kilo.ai/config.json",
|
||||
indexing: {
|
||||
enabled: true,
|
||||
provider: "ollama",
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
expect(config.indexing?.provider).toBe("ollama")
|
||||
expect(config.indexing?.enabled).toBeUndefined()
|
||||
expect(toIndexingConfigInput(config.indexing).enabled).toBe(false)
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
;(Global.Path as { config: string }).config = prev
|
||||
await clear(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user