import React, { useMemo } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { toast } from '@/components/ui'; import { isMobileDeviceViaCSS } from '@/lib/device'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { RiAddLine, RiAiAgentFill, RiAiAgentLine, RiDeleteBinLine, RiFileCopyLine, RiMore2Line, RiRobot2Line, RiRobotLine, RiRestartLine, RiEditLine } from '@remixicon/react'; import { useAgentsStore, isAgentBuiltIn, isAgentHidden, type AgentScope, type AgentDraft } from '@/stores/useAgentsStore'; import { cn } from '@/lib/utils'; import type { Agent } from '@opencode-ai/sdk/v2'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { SettingsProjectSelector } from '@/components/sections/shared/SettingsProjectSelector'; import { SidebarGroup } from '@/components/sections/shared/SidebarGroup'; interface AgentsSidebarProps { onItemSelect?: () => void; } type PermissionAction = 'allow' | 'ask' | 'deny'; type PermissionRule = { permission: string; pattern: string; action: PermissionAction }; type PermissionConfigValue = PermissionAction | Record; const toPermissionRuleset = (ruleset: unknown): PermissionRule[] => { if (!Array.isArray(ruleset)) { return []; } const parsed: PermissionRule[] = []; for (const entry of ruleset) { if (!entry || typeof entry !== 'object') { continue; } const candidate = entry as Partial; if (typeof candidate.permission !== 'string' || typeof candidate.pattern !== 'string' || typeof candidate.action !== 'string') { continue; } if (candidate.action !== 'allow' && candidate.action !== 'ask' && candidate.action !== 'deny') { continue; } parsed.push({ permission: candidate.permission, pattern: candidate.pattern, action: candidate.action }); } return parsed; }; const normalizeRuleset = (ruleset: PermissionRule[]): PermissionRule[] => { const map = new Map(); for (const rule of ruleset) { if (!rule.permission || rule.permission === 'invalid') { continue; } if (!rule.pattern) { continue; } map.set(`${rule.permission}::${rule.pattern}`, rule); } return Array.from(map.values()); }; const rulesetToPermissionConfig = (ruleset: unknown): AgentDraft['permission'] => { const parsed = normalizeRuleset(toPermissionRuleset(ruleset)); if (parsed.length === 0) { return undefined; } const byPermission: Record> = {}; for (const rule of parsed) { if (!rule.permission) { continue; } (byPermission[rule.permission] ||= {})[rule.pattern] = rule.action; } const result: Record = {}; for (const [permissionName, map] of Object.entries(byPermission)) { const patterns = Object.keys(map); if (patterns.length === 1 && patterns[0] === '*') { result[permissionName] = map['*']; continue; } result[permissionName] = map; } return Object.keys(result).length > 0 ? (result as AgentDraft['permission']) : undefined; }; export const AgentsSidebar: React.FC = ({ onItemSelect }) => { const [renameDialogAgent, setRenameDialogAgent] = React.useState(null); const [renameNewName, setRenameNewName] = React.useState(''); const [confirmActionAgent, setConfirmActionAgent] = React.useState(null); const [confirmActionType, setConfirmActionType] = React.useState<'delete' | 'reset' | null>(null); const [isConfirmActionPending, setIsConfirmActionPending] = React.useState(false); const [openMenuAgent, setOpenMenuAgent] = React.useState(null); const { selectedAgentName, agents, setSelectedAgent, setAgentDraft, createAgent, deleteAgent, loadAgents, } = useAgentsStore(); React.useEffect(() => { loadAgents(); }, [loadAgents]); const bgClass = 'bg-background'; const handleCreateNew = () => { // Generate unique name const baseName = 'new-agent'; let newName = baseName; let counter = 1; while (agents.some((a) => a.name === newName)) { newName = `${baseName}-${counter}`; counter++; } // Set draft and open the page for editing setAgentDraft({ name: newName, scope: 'user' }); setSelectedAgent(newName); onItemSelect?.(); }; const handleDeleteAgent = async (agent: Agent) => { if (isAgentBuiltIn(agent)) { toast.error('Built-in agents cannot be deleted'); return; } setConfirmActionAgent(agent); setConfirmActionType('delete'); }; const handleResetAgent = async (agent: Agent) => { if (!isAgentBuiltIn(agent)) { return; } setConfirmActionAgent(agent); setConfirmActionType('reset'); }; const closeConfirmActionDialog = () => { setConfirmActionAgent(null); setConfirmActionType(null); }; const handleConfirmAction = async () => { if (!confirmActionAgent || !confirmActionType) { return; } setIsConfirmActionPending(true); const success = await deleteAgent(confirmActionAgent.name); if (success) { if (confirmActionType === 'delete') { toast.success(`Agent "${confirmActionAgent.name}" deleted successfully`); } else { toast.success(`Agent "${confirmActionAgent.name}" reset to default`); } closeConfirmActionDialog(); } else if (confirmActionType === 'delete') { toast.error('Failed to delete agent'); } else { toast.error('Failed to reset agent'); } setIsConfirmActionPending(false); }; const handleDuplicateAgent = (agent: Agent) => { const baseName = agent.name; let copyNumber = 1; let newName = `${baseName}-copy`; while (agents.some((a) => a.name === newName)) { copyNumber++; newName = `${baseName}-copy-${copyNumber}`; } // Set draft with prefilled values from source agent const extAgent = agent as Agent & { scope?: AgentScope }; const modelStr = agent.model?.providerID && agent.model?.modelID ? `${agent.model.providerID}/${agent.model.modelID}` : null; const draftAgent = agent as Agent & { disable?: boolean }; setAgentDraft({ name: newName, scope: extAgent.scope || 'user', description: agent.description, model: modelStr, temperature: agent.temperature, top_p: agent.topP, prompt: agent.prompt, mode: agent.mode, permission: rulesetToPermissionConfig(agent.permission), disable: draftAgent.disable, }); setSelectedAgent(newName); }; const handleOpenRenameDialog = (agent: Agent) => { setRenameNewName(agent.name); setRenameDialogAgent(agent); }; const handleRenameAgent = async () => { if (!renameDialogAgent) return; const sanitizedName = renameNewName.trim().replace(/\s+/g, '-'); if (!sanitizedName) { toast.error('Agent name is required'); return; } if (sanitizedName === renameDialogAgent.name) { setRenameDialogAgent(null); return; } if (agents.some((a) => a.name === sanitizedName)) { toast.error('An agent with this name already exists'); return; } // Create new agent with new name and all existing config const renameModelStr = renameDialogAgent.model?.providerID && renameDialogAgent.model?.modelID ? `${renameDialogAgent.model.providerID}/${renameDialogAgent.model.modelID}` : null; const renameExt = renameDialogAgent as Agent & { scope?: AgentScope; disable?: boolean }; const success = await createAgent({ name: sanitizedName, description: renameDialogAgent.description, model: renameModelStr, temperature: renameDialogAgent.temperature, top_p: renameDialogAgent.topP, prompt: renameDialogAgent.prompt, mode: renameDialogAgent.mode, permission: rulesetToPermissionConfig(renameDialogAgent.permission), disable: renameExt.disable, scope: renameExt.scope, }); if (success) { // Delete old agent const deleteSuccess = await deleteAgent(renameDialogAgent.name); if (deleteSuccess) { toast.success(`Agent renamed to "${sanitizedName}"`); setSelectedAgent(sanitizedName); } else { toast.error('Failed to remove old agent after rename'); } } else { toast.error('Failed to rename agent'); } setRenameDialogAgent(null); }; const getAgentModeIcon = (mode?: string) => { switch (mode) { case 'primary': return ; case 'all': return ; case 'subagent': return ; default: return null; } }; // Filter out hidden agents (internal agents like title, compaction, summary) const visibleAgents = agents.filter((agent) => !isAgentHidden(agent)); const builtInAgents = visibleAgents.filter(isAgentBuiltIn); const customAgents = visibleAgents.filter((agent) => !isAgentBuiltIn(agent)); // Group custom agents by subfolder const { groupedCustomAgents, ungroupedCustomAgents } = useMemo(() => { const groups: Record = {}; const ungrouped: typeof customAgents = []; for (const agent of customAgents) { const ext = agent as { group?: string }; if (ext.group) { if (!groups[ext.group]) groups[ext.group] = []; groups[ext.group].push(agent); } else { ungrouped.push(agent); } } const sortedGroups = Object.keys(groups) .sort((a, b) => a.localeCompare(b)) .map((name) => ({ name, agents: groups[name] })); return { groupedCustomAgents: sortedGroups, ungroupedCustomAgents: ungrouped }; }, [customAgents]); return (

Agents

Total {visibleAgents.length}
{visibleAgents.length === 0 ? (

No agents configured

Use the + button above to create one

) : ( <> {builtInAgents.length > 0 && ( <>
Built-in Agents
{builtInAgents.map((agent) => ( { setSelectedAgent(agent.name); onItemSelect?.(); }} onReset={() => handleResetAgent(agent)} onDuplicate={() => handleDuplicateAgent(agent)} getAgentModeIcon={getAgentModeIcon} isMenuOpen={openMenuAgent === agent.name} onMenuOpenChange={(open) => setOpenMenuAgent(open ? agent.name : null)} /> ))} )} {customAgents.length > 0 && ( <>
Custom Agents
{/* Grouped agents by subfolder */} {groupedCustomAgents.map(({ name: groupName, agents: groupAgents }) => ( {groupAgents.map((agent) => ( { setSelectedAgent(agent.name); onItemSelect?.(); }} onRename={() => handleOpenRenameDialog(agent)} onDelete={() => handleDeleteAgent(agent)} onDuplicate={() => handleDuplicateAgent(agent)} getAgentModeIcon={getAgentModeIcon} isMenuOpen={openMenuAgent === agent.name} onMenuOpenChange={(open) => setOpenMenuAgent(open ? agent.name : null)} /> ))} ))} {/* Ungrouped agents (flat in root agents dir) */} {ungroupedCustomAgents.map((agent) => ( { setSelectedAgent(agent.name); onItemSelect?.(); }} onRename={() => handleOpenRenameDialog(agent)} onDelete={() => handleDeleteAgent(agent)} onDuplicate={() => handleDuplicateAgent(agent)} getAgentModeIcon={getAgentModeIcon} isMenuOpen={openMenuAgent === agent.name} onMenuOpenChange={(open) => setOpenMenuAgent(open ? agent.name : null)} /> ))} )} )}
{ if (!open && !isConfirmActionPending) { closeConfirmActionDialog(); } }} > {confirmActionType === 'delete' ? 'Delete Agent' : 'Reset Agent'} {confirmActionType === 'delete' ? `Are you sure you want to delete agent "${confirmActionAgent?.name}"?` : `Are you sure you want to reset agent "${confirmActionAgent?.name}" to its default configuration?`} {/* Rename Dialog */} !open && setRenameDialogAgent(null)}> Rename Agent Enter a new name for the agent "@{renameDialogAgent?.name}" setRenameNewName(e.target.value)} placeholder="New agent name..." className="text-foreground placeholder:text-muted-foreground" onKeyDown={(e) => { if (e.key === 'Enter') { handleRenameAgent(); } }} />
); }; interface AgentListItemProps { agent: Agent; isSelected: boolean; onSelect: () => void; onDelete?: () => void; onReset?: () => void; onRename?: () => void; onDuplicate: () => void; getAgentModeIcon: (mode?: string) => React.ReactNode; isMenuOpen: boolean; onMenuOpenChange: (open: boolean) => void; } const AgentListItem: React.FC = ({ agent, isSelected, onSelect, onDelete, onReset, onRename, onDuplicate, getAgentModeIcon, isMenuOpen, onMenuOpenChange, }) => { const extAgent = agent as Agent & { scope?: AgentScope }; const isMobile = isMobileDeviceViaCSS(); return (
{ e.preventDefault(); onMenuOpenChange(true); } : undefined} >
{onRename && ( { e.stopPropagation(); onRename(); }} > Rename )} { e.stopPropagation(); onDuplicate(); }} > Duplicate {onReset && ( { e.stopPropagation(); onReset(); }} > Reset )} {onDelete && ( { e.stopPropagation(); onDelete(); }} className="text-destructive focus:text-destructive" > Delete )}
); };