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
This commit is contained in:
Bohdan Triapitsyn
2026-06-16 13:38:24 +03:00
parent e402cd75f5
commit e982bd9388
6 changed files with 113 additions and 58 deletions
@@ -181,7 +181,7 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
} }
setIsConfirmActionPending(true); setIsConfirmActionPending(true);
const success = await deleteAgent(confirmActionAgent.name); const success = await deleteAgent(confirmActionAgent.name, (confirmActionAgent as Agent & { scope?: AgentScope }).scope);
if (success) { if (success) {
if (confirmActionType === 'delete') { if (confirmActionType === 'delete') {
@@ -276,7 +276,7 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
if (success) { if (success) {
// Delete old agent // Delete old agent
const deleteSuccess = await deleteAgent(renameDialogAgent.name); const deleteSuccess = await deleteAgent(renameDialogAgent.name, renameExt.scope);
if (deleteSuccess) { if (deleteSuccess) {
toast.success(`Agent renamed to "${sanitizedName}"`); toast.success(`Agent renamed to "${sanitizedName}"`);
setSelectedAgent(sanitizedName); setSelectedAgent(sanitizedName);
+7 -3
View File
@@ -190,7 +190,7 @@ interface AgentsStore {
loadAgents: () => Promise<boolean>; loadAgents: () => Promise<boolean>;
createAgent: (config: AgentConfig) => Promise<boolean>; createAgent: (config: AgentConfig) => Promise<boolean>;
updateAgent: (name: string, config: Partial<AgentConfig>) => Promise<boolean>; updateAgent: (name: string, config: Partial<AgentConfig>) => Promise<boolean>;
deleteAgent: (name: string) => Promise<boolean>; deleteAgent: (name: string, scope?: AgentScope) => Promise<boolean>;
getAgentByName: (name: string) => Agent | undefined; getAgentByName: (name: string) => Agent | undefined;
// Returns only visible agents (excludes hidden internal agents) // Returns only visible agents (excludes hidden internal agents)
getVisibleAgents: () => Agent[]; getVisibleAgents: () => Agent[];
@@ -448,7 +448,7 @@ export const useAgentsStore = create<AgentsStore>()(
} }
}, },
deleteAgent: async (name: string) => { deleteAgent: async (name: string, scope?: AgentScope) => {
startConfigUpdate("Deleting agent configuration…"); startConfigUpdate("Deleting agent configuration…");
let requiresReload = false; let requiresReload = false;
try { try {
@@ -458,7 +458,11 @@ export const useAgentsStore = create<AgentsStore>()(
const response = await runtimeFetch(`/api/config/agents/${encodeURIComponent(name)}${queryParams}`, { const response = await runtimeFetch(`/api/config/agents/${encodeURIComponent(name)}${queryParams}`, {
method: 'DELETE', 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); const payload = await response.json().catch(() => null);
+3 -1
View File
@@ -326,7 +326,9 @@ export async function handleConfigBridgeMessage(
} }
if (normalizedMethod === 'DELETE') { 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(); await ctx?.manager?.restart();
return { return {
id, id,
+51 -27
View File
@@ -1816,48 +1816,72 @@ export const updateAgent = (agentName: string, updates: Record<string, unknown>,
} }
}; };
export const deleteAgent = (agentName: string, workingDirectory?: string) => { const deleteJsonAgentEntry = (config: Record<string, unknown>, agentName: string): boolean => {
let deleted = false; const agentMap = config.agent as Record<string, unknown> | 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) // Check project level first (takes precedence)
if (workingDirectory) { if ((!requestedScope || requestedScope === AGENT_SCOPE.PROJECT) && workingDirectory) {
const projectPath = getProjectAgentPath(workingDirectory, agentName); const projectPath = getProjectAgentPath(workingDirectory, agentName);
if (fs.existsSync(projectPath)) { if (fs.existsSync(projectPath)) {
fs.unlinkSync(projectPath); fs.unlinkSync(projectPath);
deleted = true; resetAgentLookupCache(globalAgentLookupCache);
return;
} }
} }
// Then check user level // Then check user level
const userPath = getUserAgentPath(agentName); if (!requestedScope || requestedScope === AGENT_SCOPE.USER) {
if (fs.existsSync(userPath)) { const userPath = getUserAgentPath(agentName);
fs.unlinkSync(userPath); if (fs.existsSync(userPath)) {
deleted = true; 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) // Also check json config (highest precedence entry only)
const layers = readConfigLayers(workingDirectory);
const jsonSource = getJsonEntrySource(layers, 'agent', agentName); const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
if (jsonSource.exists && jsonSource.config && jsonSource.path) { if (jsonSource.exists && jsonSource.config && jsonSource.path && deleteJsonAgentEntry(jsonSource.config, agentName)) {
const targetConfig = jsonSource.config as Record<string, unknown>; writeConfig(jsonSource.config, jsonSource.path);
const agentMap = (targetConfig.agent as Record<string, unknown> | undefined) ?? {}; resetAgentLookupCache(globalAgentLookupCache);
delete agentMap[agentName]; return;
targetConfig.agent = agentMap;
writeConfig(targetConfig, jsonSource.path);
deleted = true;
} }
// If nothing was deleted (built-in agent), disable it in highest-precedence config throw new Error(`Agent ${agentName} is built-in or not deletable`);
if (!deleted) {
const jsonTarget = getJsonWriteTarget(layers, workingDirectory ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER);
const targetConfig = (jsonTarget.config || {}) as Record<string, unknown>;
const agentMap = (targetConfig.agent as Record<string, unknown> | undefined) ?? {};
agentMap[agentName] = { disable: true };
targetConfig.agent = agentMap;
writeConfig(targetConfig, jsonTarget.path || CONFIG_FILE);
}
resetAgentLookupCache(globalAgentLookupCache);
}; };
export const getCommandSources = (commandName: string, workingDirectory?: string): ConfigSources => { export const getCommandSources = (commandName: string, workingDirectory?: string): ConfigSources => {
+48 -24
View File
@@ -620,44 +620,68 @@ function updateAgent(agentName, updates, workingDirectory) {
console.log(`Updated agent: ${agentName} (scope: ${targetScope}, md: ${mdModified}, json: ${jsonModified})`); console.log(`Updated agent: ${agentName} (scope: ${targetScope}, md: ${mdModified}, json: ${jsonModified})`);
} }
function deleteAgent(agentName, workingDirectory) { function deleteJsonAgentEntry(config, agentName) {
const lookupCache = createAgentLookupCache(); const agentMap = config.agent;
let deleted = false; 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); const projectPath = getProjectAgentPath(workingDirectory, agentName);
if (fs.existsSync(projectPath)) { if (fs.existsSync(projectPath)) {
fs.unlinkSync(projectPath); fs.unlinkSync(projectPath);
console.log(`Deleted project-level agent .md file: ${projectPath}`); console.log(`Deleted project-level agent .md file: ${projectPath}`);
deleted = true; return;
} }
} }
const userPath = getUserAgentPath(agentName, lookupCache); if (!requestedScope || requestedScope === AGENT_SCOPE.USER) {
if (fs.existsSync(userPath)) { const userPath = getUserAgentPath(agentName, lookupCache);
fs.unlinkSync(userPath); if (fs.existsSync(userPath)) {
console.log(`Deleted user-level agent .md file: ${userPath}`); fs.unlinkSync(userPath);
deleted = true; console.log(`Deleted user-level agent .md file: ${userPath}`);
return;
}
} }
const layers = readConfigLayers(workingDirectory); const layers = readConfigLayers(workingDirectory);
const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
if (jsonSource.exists && jsonSource.config && jsonSource.path) { if (requestedScope === AGENT_SCOPE.PROJECT) {
if (!jsonSource.config.agent) jsonSource.config.agent = {}; if (layers.paths.projectPath && deleteJsonAgentEntry(layers.projectConfig, agentName)) {
delete jsonSource.config.agent[agentName]; writeConfig(layers.projectConfig, layers.paths.projectPath);
writeConfig(jsonSource.config, jsonSource.path); console.log(`Removed project-level agent from opencode.json: ${agentName}`);
console.log(`Removed agent from opencode.json: ${agentName}`); return;
deleted = true; }
throw new Error(`Project agent ${agentName} not found`);
} }
if (!deleted) { if (requestedScope === AGENT_SCOPE.USER) {
const jsonTarget = getJsonWriteTarget(layers, workingDirectory ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER); const userJsonPath = layers.paths.customPath || layers.paths.userPath;
const targetConfig = jsonTarget.config || {}; const userJsonConfig = layers.paths.customPath ? layers.customConfig : layers.userConfig;
if (!targetConfig.agent) targetConfig.agent = {}; if (userJsonPath && deleteJsonAgentEntry(userJsonConfig, agentName)) {
targetConfig.agent[agentName] = { disable: true }; writeConfig(userJsonConfig, userJsonPath);
writeConfig(targetConfig, jsonTarget.path || CONFIG_FILE); console.log(`Removed user-level agent from opencode.json: ${agentName}`);
console.log(`Disabled built-in agent: ${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 { export {
@@ -159,7 +159,8 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
return res.status(400).json({ error }); return res.status(400).json({ error });
} }
deleteAgent(agentName, directory); const scope = req.body?.scope;
deleteAgent(agentName, directory, scope);
await refreshOpenCodeAfterConfigChange('agent deletion'); await refreshOpenCodeAfterConfigChange('agent deletion');
res.json({ res.json({