diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 4de65ed3363..0c8c3a1827d 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -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 { 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() } /** diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 509568b3562..bbfec1b70e9 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -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 } diff --git a/packages/opencode/src/server/routes/kilocode.ts b/packages/opencode/src/server/routes/kilocode.ts index 3edb468fbc2..aa7d0503994 100644 --- a/packages/opencode/src/server/routes/kilocode.ts +++ b/packages/opencode/src/server/routes/kilocode.ts @@ -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) - }, - ), ) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 9fbc4210568..adb6602bade 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -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( + parameters?: { + directory?: string + workspace?: string + name?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "name" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post( + { + url: "/kilocode/agent/remove", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }, + ) + } } export class Organization extends HeyApiClient { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index fea79d1ecfa..06f61fb976f 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -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 diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 5c04569c5f1..1a53f6fef8f 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -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",