From e982bd9388405d431dc8a984212f6e3c40efdc4f Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Tue, 16 Jun 2026 13:38:24 +0300 Subject: [PATCH] fix: prevent agent deletion from disabling built-ins Stop delete from creating disable overrides Delete only the selected agent scope Keep web and VS Code behavior aligned --- .../sections/agents/AgentsSidebar.tsx | 4 +- packages/ui/src/stores/useAgentsStore.ts | 10 ++- packages/vscode/src/bridge-config-runtime.ts | 4 +- packages/vscode/src/opencodeConfig.ts | 78 ++++++++++++------- packages/web/server/lib/opencode/agents.js | 72 +++++++++++------ .../lib/opencode/config-entity-routes.js | 3 +- 6 files changed, 113 insertions(+), 58 deletions(-) diff --git a/packages/ui/src/components/sections/agents/AgentsSidebar.tsx b/packages/ui/src/components/sections/agents/AgentsSidebar.tsx index 9155eef8..c8ef2172 100644 --- a/packages/ui/src/components/sections/agents/AgentsSidebar.tsx +++ b/packages/ui/src/components/sections/agents/AgentsSidebar.tsx @@ -181,7 +181,7 @@ export const AgentsSidebar: React.FC = ({ onItemSelect }) => } setIsConfirmActionPending(true); - const success = await deleteAgent(confirmActionAgent.name); + const success = await deleteAgent(confirmActionAgent.name, (confirmActionAgent as Agent & { scope?: AgentScope }).scope); if (success) { if (confirmActionType === 'delete') { @@ -276,7 +276,7 @@ export const AgentsSidebar: React.FC = ({ onItemSelect }) => if (success) { // Delete old agent - const deleteSuccess = await deleteAgent(renameDialogAgent.name); + const deleteSuccess = await deleteAgent(renameDialogAgent.name, renameExt.scope); if (deleteSuccess) { toast.success(`Agent renamed to "${sanitizedName}"`); setSelectedAgent(sanitizedName); diff --git a/packages/ui/src/stores/useAgentsStore.ts b/packages/ui/src/stores/useAgentsStore.ts index 49923828..d52a5bfb 100644 --- a/packages/ui/src/stores/useAgentsStore.ts +++ b/packages/ui/src/stores/useAgentsStore.ts @@ -190,7 +190,7 @@ interface AgentsStore { loadAgents: () => Promise; createAgent: (config: AgentConfig) => Promise; updateAgent: (name: string, config: Partial) => Promise; - deleteAgent: (name: string) => Promise; + deleteAgent: (name: string, scope?: AgentScope) => Promise; getAgentByName: (name: string) => Agent | undefined; // Returns only visible agents (excludes hidden internal agents) getVisibleAgents: () => Agent[]; @@ -448,7 +448,7 @@ export const useAgentsStore = create()( } }, - deleteAgent: async (name: string) => { + deleteAgent: async (name: string, scope?: AgentScope) => { startConfigUpdate("Deleting agent configuration…"); let requiresReload = false; try { @@ -458,7 +458,11 @@ export const useAgentsStore = create()( const response = await runtimeFetch(`/api/config/agents/${encodeURIComponent(name)}${queryParams}`, { method: 'DELETE', - headers: configDirectory ? { 'x-opencode-directory': configDirectory } : undefined, + headers: { + 'Content-Type': 'application/json', + ...(configDirectory ? { 'x-opencode-directory': configDirectory } : {}), + }, + body: JSON.stringify({ scope }), }); const payload = await response.json().catch(() => null); diff --git a/packages/vscode/src/bridge-config-runtime.ts b/packages/vscode/src/bridge-config-runtime.ts index dcc9cfdf..361f2a1a 100644 --- a/packages/vscode/src/bridge-config-runtime.ts +++ b/packages/vscode/src/bridge-config-runtime.ts @@ -326,7 +326,9 @@ export async function handleConfigBridgeMessage( } if (normalizedMethod === 'DELETE') { - deleteAgent(agentName, workingDirectory); + const scopeValue = body?.scope as string | undefined; + const scope: AgentScope | undefined = scopeValue === 'project' ? AGENT_SCOPE.PROJECT : scopeValue === 'user' ? AGENT_SCOPE.USER : undefined; + deleteAgent(agentName, workingDirectory, scope); await ctx?.manager?.restart(); return { id, diff --git a/packages/vscode/src/opencodeConfig.ts b/packages/vscode/src/opencodeConfig.ts index 5b6e28cc..a4d8e5c7 100644 --- a/packages/vscode/src/opencodeConfig.ts +++ b/packages/vscode/src/opencodeConfig.ts @@ -1816,48 +1816,72 @@ export const updateAgent = (agentName: string, updates: Record, } }; -export const deleteAgent = (agentName: string, workingDirectory?: string) => { - let deleted = false; +const deleteJsonAgentEntry = (config: Record, agentName: string): boolean => { + const agentMap = config.agent as Record | undefined; + if (!agentMap?.[agentName]) return false; + delete agentMap[agentName]; + if (Object.keys(agentMap).length > 0) { + config.agent = agentMap; + } else { + delete config.agent; + } + return true; +}; + +export const deleteAgent = (agentName: string, workingDirectory?: string, scope?: AgentScope) => { + const requestedScope = scope === AGENT_SCOPE.PROJECT || scope === AGENT_SCOPE.USER ? scope : null; // Check project level first (takes precedence) - if (workingDirectory) { + if ((!requestedScope || requestedScope === AGENT_SCOPE.PROJECT) && workingDirectory) { const projectPath = getProjectAgentPath(workingDirectory, agentName); if (fs.existsSync(projectPath)) { fs.unlinkSync(projectPath); - deleted = true; + resetAgentLookupCache(globalAgentLookupCache); + return; } } // Then check user level - const userPath = getUserAgentPath(agentName); - if (fs.existsSync(userPath)) { - fs.unlinkSync(userPath); - deleted = true; + if (!requestedScope || requestedScope === AGENT_SCOPE.USER) { + const userPath = getUserAgentPath(agentName); + if (fs.existsSync(userPath)) { + fs.unlinkSync(userPath); + resetAgentLookupCache(globalAgentLookupCache); + return; + } + } + + const layers = readConfigLayers(workingDirectory); + + if (requestedScope === AGENT_SCOPE.PROJECT) { + if (layers.paths.projectPath && deleteJsonAgentEntry(layers.projectConfig, agentName)) { + writeConfig(layers.projectConfig, layers.paths.projectPath); + resetAgentLookupCache(globalAgentLookupCache); + return; + } + throw new Error(`Project agent ${agentName} not found`); + } + + if (requestedScope === AGENT_SCOPE.USER) { + const userJsonPath = layers.paths.customPath || layers.paths.userPath; + const userJsonConfig = layers.paths.customPath ? layers.customConfig : layers.userConfig; + if (userJsonPath && deleteJsonAgentEntry(userJsonConfig, agentName)) { + writeConfig(userJsonConfig, userJsonPath); + resetAgentLookupCache(globalAgentLookupCache); + return; + } + throw new Error(`User agent ${agentName} not found`); } // Also check json config (highest precedence entry only) - const layers = readConfigLayers(workingDirectory); const jsonSource = getJsonEntrySource(layers, 'agent', agentName); - if (jsonSource.exists && jsonSource.config && jsonSource.path) { - const targetConfig = jsonSource.config as Record; - const agentMap = (targetConfig.agent as Record | undefined) ?? {}; - delete agentMap[agentName]; - targetConfig.agent = agentMap; - writeConfig(targetConfig, jsonSource.path); - deleted = true; + if (jsonSource.exists && jsonSource.config && jsonSource.path && deleteJsonAgentEntry(jsonSource.config, agentName)) { + writeConfig(jsonSource.config, jsonSource.path); + resetAgentLookupCache(globalAgentLookupCache); + return; } - // If nothing was deleted (built-in agent), disable it in highest-precedence config - if (!deleted) { - const jsonTarget = getJsonWriteTarget(layers, workingDirectory ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER); - const targetConfig = (jsonTarget.config || {}) as Record; - const agentMap = (targetConfig.agent as Record | undefined) ?? {}; - agentMap[agentName] = { disable: true }; - targetConfig.agent = agentMap; - writeConfig(targetConfig, jsonTarget.path || CONFIG_FILE); - } - - resetAgentLookupCache(globalAgentLookupCache); + throw new Error(`Agent ${agentName} is built-in or not deletable`); }; export const getCommandSources = (commandName: string, workingDirectory?: string): ConfigSources => { diff --git a/packages/web/server/lib/opencode/agents.js b/packages/web/server/lib/opencode/agents.js index c2a25106..ff6bdf21 100644 --- a/packages/web/server/lib/opencode/agents.js +++ b/packages/web/server/lib/opencode/agents.js @@ -620,44 +620,68 @@ function updateAgent(agentName, updates, workingDirectory) { console.log(`Updated agent: ${agentName} (scope: ${targetScope}, md: ${mdModified}, json: ${jsonModified})`); } -function deleteAgent(agentName, workingDirectory) { - const lookupCache = createAgentLookupCache(); - let deleted = false; +function deleteJsonAgentEntry(config, agentName) { + const agentMap = config.agent; + if (!agentMap || typeof agentMap !== 'object' || Array.isArray(agentMap) || !agentMap[agentName]) return false; + delete agentMap[agentName]; + if (Object.keys(agentMap).length === 0) { + delete config.agent; + } + return true; +} - if (workingDirectory) { +function deleteAgent(agentName, workingDirectory, scope) { + const lookupCache = createAgentLookupCache(); + const requestedScope = scope === AGENT_SCOPE.PROJECT || scope === AGENT_SCOPE.USER ? scope : null; + + if ((!requestedScope || requestedScope === AGENT_SCOPE.PROJECT) && workingDirectory) { const projectPath = getProjectAgentPath(workingDirectory, agentName); if (fs.existsSync(projectPath)) { fs.unlinkSync(projectPath); console.log(`Deleted project-level agent .md file: ${projectPath}`); - deleted = true; + return; } } - const userPath = getUserAgentPath(agentName, lookupCache); - if (fs.existsSync(userPath)) { - fs.unlinkSync(userPath); - console.log(`Deleted user-level agent .md file: ${userPath}`); - deleted = true; + if (!requestedScope || requestedScope === AGENT_SCOPE.USER) { + const userPath = getUserAgentPath(agentName, lookupCache); + if (fs.existsSync(userPath)) { + fs.unlinkSync(userPath); + console.log(`Deleted user-level agent .md file: ${userPath}`); + return; + } } const layers = readConfigLayers(workingDirectory); - const jsonSource = getJsonEntrySource(layers, 'agent', agentName); - if (jsonSource.exists && jsonSource.config && jsonSource.path) { - if (!jsonSource.config.agent) jsonSource.config.agent = {}; - delete jsonSource.config.agent[agentName]; - writeConfig(jsonSource.config, jsonSource.path); - console.log(`Removed agent from opencode.json: ${agentName}`); - deleted = true; + + if (requestedScope === AGENT_SCOPE.PROJECT) { + if (layers.paths.projectPath && deleteJsonAgentEntry(layers.projectConfig, agentName)) { + writeConfig(layers.projectConfig, layers.paths.projectPath); + console.log(`Removed project-level agent from opencode.json: ${agentName}`); + return; + } + throw new Error(`Project agent ${agentName} not found`); } - if (!deleted) { - const jsonTarget = getJsonWriteTarget(layers, workingDirectory ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER); - const targetConfig = jsonTarget.config || {}; - if (!targetConfig.agent) targetConfig.agent = {}; - targetConfig.agent[agentName] = { disable: true }; - writeConfig(targetConfig, jsonTarget.path || CONFIG_FILE); - console.log(`Disabled built-in agent: ${agentName}`); + if (requestedScope === AGENT_SCOPE.USER) { + const userJsonPath = layers.paths.customPath || layers.paths.userPath; + const userJsonConfig = layers.paths.customPath ? layers.customConfig : layers.userConfig; + if (userJsonPath && deleteJsonAgentEntry(userJsonConfig, agentName)) { + writeConfig(userJsonConfig, userJsonPath); + console.log(`Removed user-level agent from opencode.json: ${agentName}`); + return; + } + throw new Error(`User agent ${agentName} not found`); } + + const jsonSource = getJsonEntrySource(layers, 'agent', agentName); + if (jsonSource.exists && jsonSource.config && jsonSource.path && deleteJsonAgentEntry(jsonSource.config, agentName)) { + writeConfig(jsonSource.config, jsonSource.path); + console.log(`Removed agent from opencode.json: ${agentName}`); + return; + } + + throw new Error(`Agent ${agentName} is built-in or not deletable`); } export { diff --git a/packages/web/server/lib/opencode/config-entity-routes.js b/packages/web/server/lib/opencode/config-entity-routes.js index a7be70c9..3d5886aa 100644 --- a/packages/web/server/lib/opencode/config-entity-routes.js +++ b/packages/web/server/lib/opencode/config-entity-routes.js @@ -159,7 +159,8 @@ export const registerConfigEntityRoutes = (app, dependencies) => { return res.status(400).json({ error }); } - deleteAgent(agentName, directory); + const scope = req.body?.scope; + deleteAgent(agentName, directory, scope); await refreshOpenCodeAfterConfigChange('agent deletion'); res.json({