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:
marius-kilocode
2026-07-07 12:38:12 +02:00
parent 75b994330f
commit cef3dc7ae8
19 changed files with 405 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@kilocode/cli": minor
"kilo-code": minor
---
Add a reload action that reboots the per-directory instance, picking up config, skills, agents, commands, and MCP prompts changed on disk. Sessions and history are preserved. Surfaces: `/reload` in the CLI palette and editor chat, a reload button in the task header and settings panel, the `Kilo Code: Reload Config and Skills` command, and a `POST /instance/reload` HTTP endpoint. The endpoint returns 409 while a session is actively running.
+5
View File
@@ -422,6 +422,11 @@
"command": "kilo-code.new.takeHeapSnapshot",
"title": "Take Heap Snapshot",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.reload",
"title": "Reload Config and Skills",
"category": "Kilo Code"
}
],
"submenus": [
+33
View File
@@ -1075,6 +1075,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
console.error("[Kilo New] KiloProvider: ❌ Retry connection failed:", e),
)
break
case "reload":
this.handleReload().catch((e) => console.error("[Kilo New] KiloProvider: Reload failed:", e))
break
case "openSubAgentViewer":
vscode.commands.executeCommand("kilo-code.new.openSubAgentViewer", message.sessionID, message.title)
break
@@ -3608,6 +3611,36 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
])
}
/** Reload config, skills, agents, and commands from disk by rebooting the instance. */
private async handleReload(): Promise<void> {
if (!this.client) {
console.warn("[Kilo New] handleReload: no client connection")
return
}
const dir = this.getWorkspaceDirectory(this.currentSession?.id)
try {
await this.client.instance.reload({ directory: dir }, { throwOnError: true })
} catch (err: any) {
const status = err?.response?.status
if (status === 409) {
vscode.window.showWarningMessage(
"Cannot reload while a session is running. Wait for it to finish or abort it first.",
)
} else {
console.error("[Kilo New] handleReload: reload endpoint failed:", err)
vscode.window.showErrorMessage("Reload failed. See extension logs for details.")
}
return
}
this.clearCommandsCache()
await this.reloadAfterAuthChange()
}
/** Public reload entry point for VS Code commands. */
async reload(): Promise<void> {
return this.handleReload()
}
private mapSyncEventToWebviewMessage(event: LegacySyncEvent) {
switch (event.type) {
case "message.updated": {
+6
View File
@@ -530,6 +530,12 @@ export function activate(context: vscode.ExtensionContext) {
registerHeapSnapshot(context, connectionService)
context.subscriptions.push(
vscode.commands.registerCommand("kilo-code.new.reload", () => {
provider.reload().catch((e) => console.error("[Kilo New] reload command failed:", e))
}),
)
// Register code actions (editor context menus, terminal context menus, keyboard shortcuts)
registerCodeActions(context, provider, agentManagerProvider)
registerTerminalActions(context, provider, agentManagerProvider)
@@ -212,6 +212,15 @@ export const TaskHeader: Component<TaskHeaderProps> = (props) => {
aria-label={language.t("command.session.compact")}
/>
</Tooltip>
<Tooltip value={language.t("common.reload")} placement="bottom">
<IconButton
icon="reset"
size="small"
variant="ghost"
onClick={() => vscode.postMessage({ type: "reload" })}
aria-label={language.t("common.reload")}
/>
</Tooltip>
</Show>
<Show when={hasMessages()}>
<button
@@ -143,6 +143,14 @@ const Settings: Component<SettingsProps> = (props) => {
<Button variant="secondary" size="small" icon="edit" onClick={() => open("global")}>
{language.t("settings.openGlobalConfig")}
</Button>
<Button
variant="secondary"
size="small"
icon="reset"
onClick={() => vscode.postMessage({ type: "reload" })}
>
{language.t("common.reload")}
</Button>
</div>
{/* Settings tabs */}
@@ -158,6 +158,14 @@ export function useSlashCommand(
action: sandbox.action,
enabled: sandbox.enabled,
},
{
name: "reload",
description: "Reload config, skills, agents, and commands from disk",
hints: ["refresh"],
action: () => {
vscode.postMessage({ type: "reload" })
},
},
]
const excluded = () => {
@@ -1156,6 +1156,7 @@ export const dict = {
"common.retry": "Retry",
"common.refresh": "Refresh",
"common.reload": "Reload",
"profile.title": "Profile",
"profile.notLoggedIn": "Not logged in",
@@ -924,6 +924,10 @@ export interface RetryConnectionRequest {
type: "retryConnection"
}
export interface ReloadRequest {
type: "reload"
}
// Open a sub-agent session in a read-only editor panel
export interface OpenSubAgentViewerRequest {
type: "openSubAgentViewer"
@@ -1333,6 +1337,7 @@ export type WebviewMessage =
| DiffViewerSetBaseBranchRequest
| DiffVirtualSetMarkdownRenderRequest
| RetryConnectionRequest
| ReloadRequest
| OpenSubAgentViewerRequest
| PreviewImageRequest
| SaveImageRequest
@@ -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)
})
})
+32
View File
@@ -134,6 +134,8 @@ import type {
IndexingWarningsResponses,
InstanceDisposeErrors,
InstanceDisposeResponses,
InstanceReloadErrors,
InstanceReloadResponses,
InteractiveTerminalCloseErrors,
InteractiveTerminalCloseResponses,
InteractiveTerminalGetErrors,
@@ -2257,6 +2259,36 @@ export class Instance extends HeyApiClient {
...params,
})
}
/**
* Reload instance
*
* 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.
*/
public reload<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
return (options?.client ?? this.client).post<InstanceReloadResponses, InstanceReloadErrors, ThrowOnError>({
url: "/instance/reload",
...options,
...params,
})
}
}
export class Path extends HeyApiClient {
+38
View File
@@ -2571,6 +2571,12 @@ export type KiloEmbeddingModelCatalog = {
}
}
export type ConflictError = {
_tag: "ConflictError"
message: string
resource?: string
}
export type InteractiveTerminalSnapshot = {
info: InteractiveTerminalInfo
output: string
@@ -10588,6 +10594,38 @@ export type IndexingModelsResponses = {
export type IndexingModelsResponse = IndexingModelsResponses[keyof IndexingModelsResponses]
export type InstanceReloadData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/instance/reload"
}
export type InstanceReloadErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* ConflictError
*/
409: ConflictError
}
export type InstanceReloadError = InstanceReloadErrors[keyof InstanceReloadErrors]
export type InstanceReloadResponses = {
/**
* Instance reloaded
*/
200: boolean
}
export type InstanceReloadResponse = InstanceReloadResponses[keyof InstanceReloadResponses]
export type InteractiveTerminalListData = {
body?: never
path?: never
+86
View File
@@ -13085,6 +13085,71 @@
]
}
},
"/instance/reload": {
"post": {
"tags": ["instance-reload"],
"operationId": "instance.reload",
"parameters": [
{
"name": "directory",
"in": "query",
"schema": {
"type": "string"
},
"required": false
},
{
"name": "workspace",
"in": "query",
"schema": {
"type": "string"
},
"required": false
}
],
"responses": {
"200": {
"description": "Instance reloaded",
"content": {
"application/json": {
"schema": {
"type": "boolean",
"description": "Instance reloaded"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
},
"409": {
"description": "ConflictError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConflictError"
}
}
}
}
},
"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.",
"summary": "Reload instance",
"x-codeSamples": [
{
"lang": "js",
"source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.instance.reload({\n ...\n})"
}
]
}
},
"/interactive-terminal": {
"get": {
"tags": ["interactive-terminal"],
@@ -27801,6 +27866,23 @@
"required": ["defaultModel", "models", "aliases"],
"additionalProperties": false
},
"ConflictError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["ConflictError"]
},
"message": {
"type": "string"
},
"resource": {
"type": "string"
}
},
"required": ["_tag", "message"],
"additionalProperties": false
},
"InteractiveTerminalSnapshot": {
"type": "object",
"properties": {
@@ -36967,6 +37049,10 @@
"name": "indexing",
"description": "Kilo indexing routes."
},
{
"name": "instance-reload",
"description": "Kilo instance reload route."
},
{
"name": "interactive-terminal",
"description": "Kilo human-driven interactive terminal routes."