mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
feat: add /reload action to reboot the instance from disk
Reboots the per-directory instance, reloading config, skills, agents, commands, and MCP prompts changed on disk without restarting the server. Sessions and history are preserved; only the per-directory instance caches are torn down and rebuilt. Server: POST /instance/reload wraps the existing atomic InstanceStore.reload path (the same one project.git.init uses). The rebuild completes before the 200 response, so clients can refetch with no race. Returns 409 ConflictError while a session is actively running. Emits the existing server.instance.disposed SSE event, which the TUI and extension already use to auto-refetch. CLI: /reload palette command calls the endpoint; the TUI already bootstraps on server.instance.disposed. Extension: /reload slash command, a reload button in the task header and settings panel, and a Kilo Code: Reload Config and Skills command palette entry. The handler clears the command cache and reuses reloadAfterAuthChange to re-fetch config, providers, agents, skills, and commands. Reload targets the current session's directory so Agent Manager worktree sessions reload their own worktree instance rather than the workspace root. SDK: regenerate so client.instance.reload is available to external integrations.
This commit is contained in:
@@ -15,6 +15,7 @@ import KiloSidebarPr from "@/kilocode/plugins/sidebar-pr"
|
||||
import KiloSidebarUsage from "@/kilocode/plugins/sidebar-usage"
|
||||
import KiloSandbox from "@/kilocode/plugins/sandbox"
|
||||
import KiloRemote from "@/kilocode/plugins/remote"
|
||||
import KiloReload from "@/kilocode/plugins/reload"
|
||||
// kilocode_change end
|
||||
import SidebarContext from "../feature-plugins/sidebar/context"
|
||||
import SidebarMcp from "../feature-plugins/sidebar/mcp"
|
||||
@@ -54,6 +55,7 @@ export function internalTuiPlugins(flags: Pick<RuntimeFlags.Info, "experimentalE
|
||||
KiloSidebarUsage, // kilocode_change
|
||||
KiloSandbox, // kilocode_change
|
||||
KiloRemote, // kilocode_change
|
||||
KiloReload, // kilocode_change
|
||||
HomeFooter,
|
||||
HomeTips,
|
||||
SidebarContext,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { TuiPlugin, TuiPluginModule } from "@kilocode/plugin/tui"
|
||||
|
||||
const id = "internal:reload"
|
||||
|
||||
const tui: TuiPlugin = async (api) => {
|
||||
api.keymap.registerLayer({
|
||||
commands: [
|
||||
{
|
||||
namespace: "palette",
|
||||
name: "app.reload",
|
||||
title: "Reload",
|
||||
desc: "Reload config, skills, agents, and commands from disk",
|
||||
category: "System",
|
||||
slashName: "reload",
|
||||
async run() {
|
||||
try {
|
||||
await api.client.instance.reload({}, { throwOnError: true })
|
||||
api.ui.toast({ message: "Reloaded", variant: "success" })
|
||||
} catch (err) {
|
||||
api.ui.toast({ message: String(err), variant: "error", duration: 5000 })
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const plugin: TuiPluginModule & { id: string } = { id, tui }
|
||||
|
||||
export default plugin
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Authorization } from "@/server/routes/instance/httpapi/middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "@/server/routes/instance/httpapi/middleware/instance-context"
|
||||
import {
|
||||
WorkspaceRoutingMiddleware,
|
||||
WorkspaceRoutingQuery,
|
||||
} from "@/server/routes/instance/httpapi/middleware/workspace-routing"
|
||||
import { described } from "@/server/routes/instance/httpapi/groups/metadata"
|
||||
import { ConflictError } from "@/server/routes/instance/httpapi/errors"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
|
||||
export const ReloadPaths = {
|
||||
reload: "/instance/reload",
|
||||
} as const
|
||||
|
||||
export const InstanceReloadApi = HttpApi.make("instance-reload")
|
||||
.add(
|
||||
HttpApiGroup.make("instance-reload")
|
||||
.add(
|
||||
HttpApiEndpoint.post("reload", ReloadPaths.reload, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Instance reloaded"),
|
||||
error: ConflictError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "instance.reload",
|
||||
summary: "Reload instance",
|
||||
description:
|
||||
"Atomically dispose and reboot the current Kilo instance, reloading config, skills, agents, commands, and MCP prompts from disk. Returns 409 if a session is actively running.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "instance-reload",
|
||||
description: "Kilo instance reload route.",
|
||||
}),
|
||||
)
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(WorkspaceRoutingMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "kilo HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Kilo HttpApi surface.",
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as InstanceState from "@/effect/instance-state"
|
||||
import { SessionStatus } from "@/session/status"
|
||||
import { ConflictError } from "@/server/routes/instance/httpapi/errors"
|
||||
import { markInstanceForReload } from "@/server/routes/instance/httpapi/lifecycle"
|
||||
import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import type { SessionID } from "@/session/schema"
|
||||
|
||||
export function hasBusySession(statuses: Map<SessionID, SessionStatus.Info>): boolean {
|
||||
for (const info of statuses.values()) {
|
||||
if (info.type === "busy") return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const instanceReloadHandlers = HttpApiBuilder.group(InstanceHttpApi, "instance-reload", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const status = yield* SessionStatus.Service
|
||||
|
||||
const reload = Effect.fn("InstanceReloadHttpApi.reload")(function* () {
|
||||
const ctx = yield* InstanceState.context
|
||||
if (hasBusySession(yield* status.list())) {
|
||||
return yield* Effect.fail(
|
||||
new ConflictError({
|
||||
message: "Cannot reload while a session is running. Wait for it to finish or abort it first.",
|
||||
}),
|
||||
)
|
||||
}
|
||||
yield* markInstanceForReload(ctx, {
|
||||
directory: ctx.directory,
|
||||
worktree: ctx.worktree,
|
||||
project: ctx.project,
|
||||
})
|
||||
return true
|
||||
})
|
||||
|
||||
return handlers.handle("reload", reload)
|
||||
}),
|
||||
)
|
||||
@@ -15,6 +15,7 @@ import { commitMessageHandlers } from "./handlers/commit-message"
|
||||
import { configConsoleHandlers } from "./handlers/config-console"
|
||||
import { enhancePromptHandlers } from "./handlers/enhance-prompt"
|
||||
import { indexingHandlers } from "./handlers/indexing"
|
||||
import { instanceReloadHandlers } from "./handlers/instance-reload"
|
||||
import { interactiveTerminalHandlers } from "./handlers/interactive-terminal"
|
||||
import { kiloGatewayHandlers } from "./handlers/kilo-gateway"
|
||||
import { kilocodeHandlers } from "./handlers/kilocode"
|
||||
@@ -35,6 +36,7 @@ export const provide = Layer.provide([
|
||||
configConsoleHandlers,
|
||||
enhancePromptHandlers,
|
||||
indexingHandlers,
|
||||
instanceReloadHandlers,
|
||||
interactiveTerminalHandlers,
|
||||
kiloGatewayHandlers,
|
||||
kilocodeHandlers,
|
||||
|
||||
@@ -28,6 +28,7 @@ import { BackgroundProcessApi } from "@/kilocode/server/httpapi/groups/backgroun
|
||||
import { ConfigConsoleApi } from "@/kilocode/server/httpapi/groups/config-console"
|
||||
import { EnhancePromptApi } from "@/kilocode/server/httpapi/groups/enhance-prompt"
|
||||
import { IndexingApi } from "@/kilocode/server/httpapi/groups/indexing"
|
||||
import { InstanceReloadApi } from "@/kilocode/server/httpapi/groups/instance-reload"
|
||||
import { InteractiveTerminalApi } from "@/kilocode/server/httpapi/groups/interactive-terminal"
|
||||
import { KiloGatewayApi } from "@/kilocode/server/httpapi/groups/kilo-gateway"
|
||||
import { KilocodeApi } from "@/kilocode/server/httpapi/groups/kilocode"
|
||||
@@ -76,6 +77,7 @@ export const InstanceHttpApi = HttpApi.make("opencode-instance")
|
||||
.addHttpApi(ConfigConsoleApi)
|
||||
.addHttpApi(EnhancePromptApi)
|
||||
.addHttpApi(IndexingApi)
|
||||
.addHttpApi(InstanceReloadApi)
|
||||
.addHttpApi(InteractiveTerminalApi)
|
||||
.addHttpApi(KiloGatewayApi)
|
||||
.addHttpApi(KilocodeApi)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { hasBusySession } from "@/kilocode/server/httpapi/handlers/instance-reload"
|
||||
import type { SessionStatus } from "@/session/status"
|
||||
import { SessionID } from "@/session/schema"
|
||||
|
||||
const entries = (items: [string, SessionStatus.Info][]) =>
|
||||
items.map(([k, v]) => [k as SessionID, v] as [SessionID, SessionStatus.Info])
|
||||
|
||||
describe("instance-reload hasBusySession", () => {
|
||||
it("returns false for an empty map", () => {
|
||||
expect(hasBusySession(new Map(entries([])))).toBe(false)
|
||||
})
|
||||
|
||||
it("returns false when all sessions are idle", () => {
|
||||
expect(
|
||||
hasBusySession(
|
||||
new Map(
|
||||
entries([
|
||||
["s1", { type: "idle" }],
|
||||
["s2", { type: "idle" }],
|
||||
]),
|
||||
),
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("returns true when any session is busy", () => {
|
||||
expect(
|
||||
hasBusySession(
|
||||
new Map(
|
||||
entries([
|
||||
["s1", { type: "idle" }],
|
||||
["s2", { type: "busy" }],
|
||||
]),
|
||||
),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("returns true when the only session is busy", () => {
|
||||
expect(hasBusySession(new Map(entries([["s1", { type: "busy" }]])))).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user