feat: add ability to remove custom modes from Agent Behaviour settings - #7100
Conversation
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
b20553e to
b90a223
Compare
…igrator Agent.remove() now also scans the .kilocodemodes YAML files that ModesMigrator reads (VSCode global storage, ~/.kilocode/cli/global/, ~/.kilocodemodes, project .kilocodemodes) and removes the matching entry by slug, so migrated modes don't reappear after deletion.
aff7820 to
b8e196d
Compare
Add a removing guard signal that prevents confirmRemoveMode from firing during the 200ms window after clicking Remove. This avoids a race condition where the dialog close animation + reactive list re-render causes the next list item's click handler to fire.
61ca043 to
a80d495
Compare
Replace the removing guard signal with a simpler approach: delay the optimistic list removal by 150ms so it happens after the dialog's 100ms close animation completes. This prevents both the double-dialog issue and the stuck-state issue.
489607e to
a406741
Compare
| )) | ||
| } | ||
|
|
||
| const removableModes = createMemo(() => session.agents().filter((a) => !a.native)) |
There was a problem hiding this comment.
WARNING: Delete button is shown for modes this API cannot remove
This filter treats every non-native agent as removable, but Agent.remove() only deletes markdown-backed agents or legacy .kilocodemodes entries. Modes declared directly in kilo.json/opencode.json are also native: false, so the UI will offer a remove action that always comes back with no agent file found on disk.
There was a problem hiding this comment.
I'm willing to accept this for now, we need to iterate on this screen anyways. it's good enough for supporting the marketplace
| const [skills, setSkills] = createSignal<SkillInfo[]>([]) | ||
|
|
||
| const removeMode = (name: string) => { | ||
| setAgents((prev) => prev.filter((a) => a.name !== name)) |
There was a problem hiding this comment.
WARNING: Removed mode can stay selected in session state
removeMode() only updates the agents list. If the user deletes the mode currently stored in agentSelections or pendingAgentSelection, selectedAgentName() keeps returning that stale value, and the next sendMessage() still forwards it to the backend. Agent.get(input.agent) then resolves undefined in session/prompt.ts, which turns this into a runtime failure instead of falling back to the new default mode.
|
|
||
| // 1. Delete .md files from config directories | ||
| const dirs = await Config.directories() | ||
| const patterns = ["{agent,agents}/**/" + name + ".md", "{mode,modes}/" + name + ".md"] |
There was a problem hiding this comment.
WARNING: File lookup assumes the agent name matches the markdown filename
loadAgent() and loadMode() let frontmatter override name, and the webview posts back agent.name here. If a custom agent is defined in foo.md with name: "bar", this pattern searches for bar.md and never finds the backing file, so deleting renamed agents fails even though they appear removable in the UI.
There was a problem hiding this comment.
please don't do this 😅 . again, acceptable for now
Code Review SummaryStatus: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Other Observations (not in diff)No additional issues outside the diff. Files Reviewed (26 files)
Fix these issues in Kilo Cloud Reviewed by gpt-5.4-20260305 · 1,514,317 tokens |
When removeMode() is called, also clear any agentSelections entries and pendingAgentSelection that reference the removed mode. Without this, selectedAgentName() keeps returning the stale value and sendMessage() forwards it to the backend where Agent.get() resolves undefined.
| } | ||
| // Invalidate cache so next requestAgents fetches fresh data | ||
| this.cachedAgentsMessage = null | ||
| await this.fetchAndSendAgents() |
There was a problem hiding this comment.
WARNING: Removed mode leaves stale config references behind
This only refreshes agentsLoaded. If the deleted mode is still referenced by config.default_agent or config.agent, the webview keeps showing it from stale config state and the backend can start throwing default agent "<name>" not found when the next prompt falls back to the default agent. Refresh or clear the config after a successful removal as well.
| const patterns = ["{agent,agents}/**/" + name + ".md", "{mode,modes}/" + name + ".md"] | ||
| for (const dir of dirs) { | ||
| for (const pattern of patterns) { | ||
| const matches = await Glob.scan(pattern, { cwd: dir, absolute: true, dot: true }) |
There was a problem hiding this comment.
WARNING: Agent name is used as an unescaped glob
name comes straight from the request body and is interpolated into the glob pattern here. Because custom agent names are not validated to exclude glob metacharacters, a mode named with *, ?, [], or {} can match and delete unrelated .md agent files. Escape the slug before calling Glob.scan, or build the expected path directly.
…modes feat: add ability to remove custom modes from Agent Behaviour settings
…modes feat: add ability to remove custom modes from Agent Behaviour settings
Summary
{ agent: { [name]: { disable: true } } }) and refreshes the agent list