fix(cli): prevent stale global config reloads

This commit is contained in:
marius-kilocode
2026-05-27 15:13:51 +02:00
parent cb933859ff
commit dcfadac83e
4 changed files with 106 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Prevent saved global indexing provider changes from temporarily reverting in active workspaces.
@@ -194,9 +194,11 @@ export const GlobalRoutes = lazy(() =>
const config = c.req.valid("json")
const result = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.updateGlobal(config)))
if (result.changed) {
void AppRuntime.runPromise(disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true })).catch(
() => undefined,
// kilocode_change start
await AppRuntime.runPromise(
disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true }).pipe(Effect.catchCause(() => Effect.void)),
)
// kilocode_change end
}
return c.json(result.info)
},
@@ -86,7 +86,13 @@ export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handl
const configUpdate = Effect.fn("GlobalHttpApi.configUpdate")(function* (ctx) {
const result = yield* config.updateGlobal(ctx.payload)
if (result.changed) bridge.fork(disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true }))
// kilocode_change start
if (result.changed) {
yield* bridge.run(
disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true }).pipe(Effect.catchCause(() => Effect.void)),
)
}
// kilocode_change end
return result.info
})
@@ -0,0 +1,90 @@
import { afterEach, describe, expect, test } from "bun:test"
import { Global } from "@opencode-ai/core/global"
import { Flag } from "@opencode-ai/core/flag/flag"
import { GlobalBus } from "../../src/bus/global"
import { Server } from "../../src/server/server"
import { registerDisposer } from "../../src/effect/instance-registry"
import * as Log from "@opencode-ai/core/util/log"
import { resetDatabase } from "../fixture/db"
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
void Log.init({ print: false })
const experimental = Flag.KILO_EXPERIMENTAL_HTTPAPI
const root = Global.Path.config
function app(value: boolean) {
Flag.KILO_EXPERIMENTAL_HTTPAPI = value
return value ? Server.Default().app : Server.Legacy().app
}
async function update(target: ReturnType<typeof app>, provider: "kilo" | "openrouter") {
return target.request("/global/config", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ indexing: { provider } }),
})
}
async function provider(target: ReturnType<typeof app>, directory: string) {
const response = await target.request("/config", { headers: { "x-kilo-directory": directory } })
return (await response.json()).indexing?.provider as string | undefined
}
afterEach(async () => {
Flag.KILO_EXPERIMENTAL_HTTPAPI = experimental
;(Global.Path as { config: string }).config = root
await disposeAllInstances()
await resetDatabase()
})
describe("global config refresh", () => {
for (const value of [false, true]) {
test(`${value ? "httpapi" : "legacy"} update reloads existing instance before responding`, async () => {
await using config = await tmpdir()
await using workspace = await tmpdir({ config: { formatter: false, lsp: false } })
;(Global.Path as { config: string }).config = config.path
await disposeAllInstances()
const target = app(value)
expect((await update(target, "openrouter")).status).toBe(200)
expect(await provider(target, workspace.path)).toBe("openrouter")
const started = Promise.withResolvers<void>()
const release = Promise.withResolvers<void>()
const unregister = registerDisposer(async (directory) => {
if (directory !== workspace.path) return
started.resolve()
await release.promise
})
try {
const pending = update(target, "kilo")
await started.promise
const early = await Promise.race([pending.then(() => true), Bun.sleep(10).then(() => false)])
expect(early).toBe(false)
release.resolve()
expect((await pending).status).toBe(200)
expect(await provider(target, workspace.path)).toBe("kilo")
} finally {
release.resolve()
unregister()
}
})
test(`${value ? "httpapi" : "legacy"} update ignores disposal notification failures`, async () => {
await using config = await tmpdir()
;(Global.Path as { config: string }).config = config.path
await disposeAllInstances()
const target = app(value)
const listener = () => {
throw new Error("listener failed")
}
GlobalBus.on("event", listener)
try {
expect((await update(target, "kilo")).status).toBe(200)
} finally {
GlobalBus.off("event", listener)
}
})
}
})