diff --git a/.changeset/steady-indexing-provider.md b/.changeset/steady-indexing-provider.md new file mode 100644 index 00000000000..9afaebdf819 --- /dev/null +++ b/.changeset/steady-indexing-provider.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent saved global indexing provider changes from temporarily reverting in active workspaces. diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts index 5e83c305807..eb783aacca6 100644 --- a/packages/opencode/src/server/routes/global.ts +++ b/packages/opencode/src/server/routes/global.ts @@ -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) }, diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts index f80869b64d3..c9f202fd9bc 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts @@ -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 }) diff --git a/packages/opencode/test/kilocode/global-config-refresh.test.ts b/packages/opencode/test/kilocode/global-config-refresh.test.ts new file mode 100644 index 00000000000..ba7521f0bf6 --- /dev/null +++ b/packages/opencode/test/kilocode/global-config-refresh.test.ts @@ -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, 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, 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() + const release = Promise.withResolvers() + 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) + } + }) + } +})