feat: add backend route to delete custom mode files from disk

Replace the config disable:true approach with a proper Agent.remove()
function that scans config directories for the agent's markdown file
and deletes it, then refreshes state via Instance.dispose().

- Add Agent.remove() in agent.ts with RemoveError
- Add POST /kilocode/agent/remove route
- Mount KilocodeRoutes in server.ts
- Update KiloProvider to call the new backend route
- Regenerate SDK
This commit is contained in:
Mark IJbema
2026-03-16 15:54:45 +01:00
parent bcf609a55d
commit 0b702ea1d7
6 changed files with 254 additions and 33 deletions
+13 -8
View File
@@ -1162,25 +1162,30 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
/**
* Remove (disable) a mode via config, then refresh agents.
* Remove a custom mode via the CLI backend (deletes from disk + refreshes state).
* The webview optimistically removes the mode from its list before this runs.
* On failure, re-fetches agents so the webview reverts to the authoritative state.
*/
private async handleRemoveMode(name: string): Promise<void> {
if (!this.client) return
try {
await this.client.global.config.update(
{ config: { agent: { [name]: { disable: true } } } },
{ throwOnError: true },
)
// Invalidate cache so next requestAgents fetches fresh data
this.cachedAgentsMessage = null
await this.fetchAndSendAgents()
const dir = this.getWorkspaceDirectory()
const result = await this.client.kilocode.removeAgent({ name, directory: dir })
if (result.error) {
console.error("[Kilo New] KiloProvider: removeAgent returned error:", result.error)
this.cachedAgentsMessage = null
await this.fetchAndSendAgents()
return
}
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to remove mode:", error)
this.cachedAgentsMessage = null
await this.fetchAndSendAgents()
return
}
// Invalidate cache so next requestAgents fetches fresh data
this.cachedAgentsMessage = null
await this.fetchAndSendAgents()
}
/**
+47
View File
@@ -17,6 +17,8 @@ import PROMPT_ORCHESTRATOR from "./prompt/orchestrator.txt"
import PROMPT_SUMMARY from "./prompt/summary.txt"
import PROMPT_TITLE from "./prompt/title.txt"
import { PermissionNext } from "@/permission/next"
import { NamedError } from "@opencode-ai/util/error" // kilocode_change
import { Glob } from "../util/glob" // kilocode_change
import { mergeDeep, pipe, sortBy, values } from "remeda"
import { Global } from "@/global"
import path from "path"
@@ -438,4 +440,49 @@ export namespace Agent {
const result = await generateObject(params)
return result.object
}
// kilocode_change start
export const RemoveError = NamedError.create(
"AgentRemoveError",
z.object({
name: z.string(),
message: z.string(),
}),
)
/**
* Remove a custom agent by deleting its markdown file from disk.
* Scans all config directories for agent/mode .md files matching the name.
*/
export async function remove(name: string) {
const agents = await state()
const agent = agents[name]
if (!agent) throw new RemoveError({ name, message: "agent not found" })
if (agent.native) throw new RemoveError({ name, message: "cannot remove native agent" })
const dirs = await Config.directories()
const patterns = ["{agent,agents}/**/" + name + ".md", "{mode,modes}/" + name + ".md"]
let found = false
for (const dir of dirs) {
for (const pattern of patterns) {
const matches = await Glob.scan(pattern, { cwd: dir, absolute: true, dot: true })
for (const file of matches) {
await Bun.file(file)
.exists()
.then(async (exists) => {
if (!exists) return
const { unlink } = await import("fs/promises")
await unlink(file)
found = true
})
}
}
}
if (!found) throw new RemoveError({ name, message: "no agent file found on disk" })
await Instance.dispose()
}
// kilocode_change end
}
+57 -25
View File
@@ -5,38 +5,70 @@ import { Hono } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
import { Skill } from "../../skill/skill"
import { Agent } from "../../agent/agent"
import { lazy } from "../../util/lazy"
import { errors } from "../error"
export const KilocodeRoutes = lazy(() =>
new Hono().post(
"/skill/remove",
describeRoute({
summary: "Remove a skill",
description: "Remove a skill by deleting its directory from disk and clearing it from cache.",
operationId: "kilocode.removeSkill",
responses: {
200: {
description: "Skill removed",
content: {
"application/json": {
schema: resolver(z.boolean()),
new Hono()
.post(
"/skill/remove",
describeRoute({
summary: "Remove a skill",
description: "Remove a skill by deleting its directory from disk and clearing it from cache.",
operationId: "kilocode.removeSkill",
responses: {
200: {
description: "Skill removed",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
...errors(400),
},
}),
validator(
"json",
z.object({
location: z.string(),
}),
validator(
"json",
z.object({
location: z.string(),
}),
),
async (c) => {
const { location } = c.req.valid("json")
await Skill.remove(location)
return c.json(true)
},
)
.post(
"/agent/remove",
describeRoute({
summary: "Remove a custom agent",
description: "Remove a custom (non-native) agent by deleting its markdown file from disk and refreshing state.",
operationId: "kilocode.removeAgent",
responses: {
200: {
description: "Agent removed",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"json",
z.object({
name: z.string(),
}),
),
async (c) => {
const { name } = c.req.valid("json")
await Agent.remove(name)
return c.json(true)
},
),
async (c) => {
const { location } = c.req.valid("json")
await Skill.remove(location)
return c.json(true)
},
),
)
+41
View File
@@ -57,6 +57,8 @@ import type {
KiloCloudSessionImportResponses,
KiloCloudSessionsErrors,
KiloCloudSessionsResponses,
KilocodeRemoveAgentErrors,
KilocodeRemoveAgentResponses,
KilocodeRemoveSkillErrors,
KilocodeRemoveSkillResponses,
KiloFimErrors,
@@ -2969,6 +2971,45 @@ export class Kilocode extends HeyApiClient {
},
)
}
/**
* Remove a custom agent
*
* Remove a custom (non-native) agent by deleting its markdown file from disk and refreshing state.
*/
public removeAgent<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
name?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
{ in: "body", key: "name" },
],
},
],
)
return (options?.client ?? this.client).post<KilocodeRemoveAgentResponses, KilocodeRemoveAgentErrors, ThrowOnError>(
{
url: "/kilocode/agent/remove",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
},
)
}
}
export class Organization extends HeyApiClient {
+30
View File
@@ -4432,6 +4432,36 @@ export type KilocodeRemoveSkillResponses = {
export type KilocodeRemoveSkillResponse = KilocodeRemoveSkillResponses[keyof KilocodeRemoveSkillResponses]
export type KilocodeRemoveAgentData = {
body?: {
name: string
}
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/kilocode/agent/remove"
}
export type KilocodeRemoveAgentErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type KilocodeRemoveAgentError = KilocodeRemoveAgentErrors[keyof KilocodeRemoveAgentErrors]
export type KilocodeRemoveAgentResponses = {
/**
* Agent removed
*/
200: boolean
}
export type KilocodeRemoveAgentResponse = KilocodeRemoveAgentResponses[keyof KilocodeRemoveAgentResponses]
export type KiloProfileData = {
body?: never
path?: never
+66
View File
@@ -5438,6 +5438,72 @@
]
}
},
"/kilocode/agent/remove": {
"post": {
"operationId": "kilocode.removeAgent",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "workspace",
"schema": {
"type": "string"
}
}
],
"summary": "Remove a custom agent",
"description": "Remove a custom (non-native) agent by deleting its markdown file from disk and refreshing state.",
"responses": {
"200": {
"description": "Agent removed",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": ["name"]
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.kilocode.removeAgent({\n ...\n})"
}
]
}
},
"/kilo/profile": {
"get": {
"operationId": "kilo.profile",