refactor: hot reload kilo console configs

This commit is contained in:
Catriel Müller
2026-06-12 18:07:36 -03:00
parent f82828a02f
commit 2f69c132b0
6 changed files with 117 additions and 4 deletions
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Show Agent Manager and other Git worktrees in the Kilo Console project view.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Keep Kilo Console terminal sessions open when changing diff layout and other console preferences.
@@ -79,8 +79,9 @@ export const configConsoleHandlers = HttpApiBuilder.group(InstanceHttpApi, "conf
return yield* config.get()
}
if (body.scope === "global") {
const result = yield* config.updateGlobal(patch)
if (result.changed) {
const hot = Object.keys(patch).every((key) => key === "console")
const result = yield* config.updateGlobal(patch, hot ? { dispose: false } : undefined)
if (result.changed && !hot) {
yield* disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true }).pipe(
Effect.catchCause(() => Effect.void),
)
@@ -112,10 +112,13 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
return yield* registry.ids()
})
// kilocode_change start - discover Agent Manager and external git worktrees
const worktree = Effect.fn("ExperimentalHttpApi.worktree")(function* () {
const ctx = yield* InstanceState.context
return yield* project.sandboxes(ctx.project.id)
return yield* mapWorktreeError(worktreeSvc.list()).pipe(
Effect.map((items) => items.map((item) => item.directory)),
)
})
// kilocode_change end
const worktreeCreate = Effect.fn("ExperimentalHttpApi.worktreeCreate")(function* (ctx: {
payload: Worktree.CreateInput | undefined
@@ -6,12 +6,14 @@ import { Server } from "../../../src/server/server"
import { Config } from "../../../src/config/config"
import { KilocodeConfigOverlay } from "../../../src/kilocode/config/overlay"
import { Permission } from "../../../src/permission"
import { PtyPaths } from "../../../src/server/routes/instance/httpapi/groups/pty"
import { resetDatabase } from "../../fixture/db"
import { disposeAllInstances, tmpdir } from "../../fixture/fixture"
void Log.init({ print: false })
const original = Global.Path.config
const terminal = process.platform === "win32" ? test.skip : test.serial
type Overlay = {
fields: Record<string, { source: string; inherited: boolean; overridden: boolean; value?: unknown }>
@@ -323,6 +325,35 @@ describe("config overlay routes", () => {
)
})
terminal("preserves active terminals after updating global console preferences", async () => {
await using global = await tmpdir()
await using project = await tmpdir()
;(Global.Path as { config: string }).config = global.path
const headers = { "x-kilo-directory": project.path }
const created = await Server.Default().app.request(PtyPaths.create, {
method: "POST",
headers: { ...headers, "content-type": "application/json" },
body: JSON.stringify({ command: "/usr/bin/env", args: ["sh", "-c", "sleep 30"], title: "console" }),
})
const info = await json<{ id: string }>(created)
try {
await json(
await request(Server.Default().app, undefined, "/config/overlay", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ scope: "global", set: { console: { diff_style: "split" } } }),
}),
)
const found = await Server.Default().app.request(PtyPaths.get.replace(":ptyID", info.id), { headers })
expect(found.status).toBe(200)
expect(await found.json()).toMatchObject({ id: info.id, title: "console", status: "running" })
} finally {
await Server.Default().app.request(PtyPaths.remove.replace(":ptyID", info.id), { method: "DELETE", headers })
}
})
for (const value of [false, true]) {
test.serial(
`${value ? "httpapi" : "legacy"} global overlay update refreshes existing project instances without a project directory`,
@@ -0,0 +1,68 @@
import { $ } from "bun"
import { describe, expect } from "bun:test"
import path from "path"
import { Effect, Layer, Schema } from "effect"
import { HttpRouter } from "effect/unstable/http"
import { Flag } from "@opencode-ai/core/flag/flag"
import { HttpApiApp } from "../../../src/server/routes/instance/httpapi/server"
import { ExperimentalPaths } from "../../../src/server/routes/instance/httpapi/groups/experimental"
import { resetDatabase } from "../../fixture/db"
import { TestInstance } from "../../fixture/fixture"
import { testEffect } from "../../lib/effect"
const state = Layer.effectDiscard(
Effect.gen(function* () {
const original = Flag.KILO_EXPERIMENTAL_WORKSPACES
Flag.KILO_EXPERIMENTAL_WORKSPACES = true
yield* Effect.addFinalizer(() =>
Effect.promise(async () => {
Flag.KILO_EXPERIMENTAL_WORKSPACES = original
await resetDatabase()
}),
)
}),
)
const it = testEffect(state)
const run = process.platform === "win32" ? it.instance.skip : it.instance
type Server = ReturnType<typeof HttpRouter.toWebHandler>
function serve() {
return Effect.acquireRelease(
Effect.sync(() => HttpRouter.toWebHandler(HttpApiApp.routes, { disableLogger: true })),
(server) => Effect.promise(() => server.dispose()).pipe(Effect.ignore),
)
}
function request(server: Server, input: string) {
return Effect.promise(() => server.handler(new Request(new URL(input, "http://localhost")), HttpApiApp.context))
}
describe("Kilo Console worktree listing", () => {
run(
"lists worktrees created by Agent Manager",
() =>
Effect.gen(function* () {
const test = yield* TestInstance
const server = yield* serve()
const directory = path.join(test.directory, ".kilo", "worktrees", "console-list")
yield* Effect.promise(() =>
$`git worktree add --quiet -b console-list ${directory} HEAD`.cwd(test.directory).quiet(),
)
const response = yield* request(
server,
`${ExperimentalPaths.worktree}?directory=${encodeURIComponent(test.directory)}`,
)
expect(response.status).toBe(200)
const worktrees = Schema.decodeUnknownSync(Schema.Array(Schema.String))(
yield* Effect.promise(() => response.json()),
)
expect(worktrees).toContain(directory)
expect(worktrees).not.toContain(test.directory)
}),
{ git: true },
)
})