import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import { useUI } from '../context/UIContext'; type SubagentProfile = { agent_id: string; name?: string; role?: string; system_prompt?: string; tool_allowlist?: string[]; memory_namespace?: string; max_retries?: number; retry_backoff_ms?: number; timeout_sec?: number; max_task_chars?: number; max_result_chars?: number; status?: 'active' | 'disabled' | string; created_at?: number; updated_at?: number; }; type ToolAllowlistGroup = { name: string; description?: string; aliases?: string[]; tools?: string[]; }; const emptyDraft: SubagentProfile = { agent_id: '', name: '', role: '', system_prompt: '', memory_namespace: '', status: 'active', tool_allowlist: [], max_retries: 0, retry_backoff_ms: 1000, timeout_sec: 0, max_task_chars: 0, max_result_chars: 0, }; const SubagentProfiles: React.FC = () => { const { t } = useTranslation(); const { q } = useAppContext(); const ui = useUI(); const [items, setItems] = useState([]); const [selectedId, setSelectedId] = useState(''); const [draft, setDraft] = useState(emptyDraft); const [saving, setSaving] = useState(false); const [groups, setGroups] = useState([]); const selected = useMemo( () => items.find((p) => p.agent_id === selectedId) || null, [items, selectedId], ); const load = async () => { const r = await fetch(`/webui/api/subagent_profiles${q}`); if (!r.ok) throw new Error(await r.text()); const j = await r.json(); const profiles = Array.isArray(j.profiles) ? j.profiles : []; setItems(profiles); if (profiles.length === 0) { setSelectedId(''); setDraft(emptyDraft); return; } const keep = profiles.find((p: SubagentProfile) => p.agent_id === selectedId); const next = keep || profiles[0]; setSelectedId(next.agent_id || ''); setDraft({ agent_id: next.agent_id || '', name: next.name || '', role: next.role || '', system_prompt: next.system_prompt || '', memory_namespace: next.memory_namespace || '', status: (next.status as string) || 'active', tool_allowlist: Array.isArray(next.tool_allowlist) ? next.tool_allowlist : [], max_retries: Number(next.max_retries || 0), retry_backoff_ms: Number(next.retry_backoff_ms || 1000), timeout_sec: Number(next.timeout_sec || 0), max_task_chars: Number(next.max_task_chars || 0), max_result_chars: Number(next.max_result_chars || 0), }); }; useEffect(() => { load().catch(() => {}); }, [q]); useEffect(() => { const loadGroups = async () => { const r = await fetch(`/webui/api/tool_allowlist_groups${q}`); if (!r.ok) return; const j = await r.json(); const arr = Array.isArray(j.groups) ? j.groups : []; setGroups(arr); }; loadGroups().catch(() => {}); }, [q]); const onSelect = (p: SubagentProfile) => { setSelectedId(p.agent_id || ''); setDraft({ agent_id: p.agent_id || '', name: p.name || '', role: p.role || '', system_prompt: p.system_prompt || '', memory_namespace: p.memory_namespace || '', status: (p.status as string) || 'active', tool_allowlist: Array.isArray(p.tool_allowlist) ? p.tool_allowlist : [], max_retries: Number(p.max_retries || 0), retry_backoff_ms: Number(p.retry_backoff_ms || 1000), timeout_sec: Number(p.timeout_sec || 0), max_task_chars: Number(p.max_task_chars || 0), max_result_chars: Number(p.max_result_chars || 0), }); }; const onNew = () => { setSelectedId(''); setDraft(emptyDraft); }; const parseAllowlist = (text: string): string[] => { return text .split(',') .map((x) => x.trim()) .filter((x) => x.length > 0); }; const allowlistText = (draft.tool_allowlist || []).join(', '); const addAllowlistToken = (token: string) => { const list = Array.isArray(draft.tool_allowlist) ? [...draft.tool_allowlist] : []; if (!list.includes(token)) { list.push(token); setDraft({ ...draft, tool_allowlist: list }); } }; const save = async () => { const agentId = String(draft.agent_id || '').trim(); if (!agentId) { await ui.notify({ title: t('requestFailed'), message: 'agent_id is required' }); return; } setSaving(true); try { const action = selected ? 'update' : 'create'; const r = await fetch(`/webui/api/subagent_profiles${q}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, agent_id: agentId, name: draft.name || '', role: draft.role || '', system_prompt: draft.system_prompt || '', memory_namespace: draft.memory_namespace || '', status: draft.status || 'active', tool_allowlist: draft.tool_allowlist || [], max_retries: Number(draft.max_retries || 0), retry_backoff_ms: Number(draft.retry_backoff_ms || 0), timeout_sec: Number(draft.timeout_sec || 0), max_task_chars: Number(draft.max_task_chars || 0), max_result_chars: Number(draft.max_result_chars || 0), }), }); if (!r.ok) { await ui.notify({ title: t('requestFailed'), message: await r.text() }); return; } await load(); setSelectedId(agentId); } finally { setSaving(false); } }; const setStatus = async (status: 'active' | 'disabled') => { const agentId = String(draft.agent_id || '').trim(); if (!agentId) return; const action = status === 'active' ? 'enable' : 'disable'; const r = await fetch(`/webui/api/subagent_profiles${q}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, agent_id: agentId }), }); if (!r.ok) { await ui.notify({ title: t('requestFailed'), message: await r.text() }); return; } await load(); }; const remove = async () => { const agentId = String(draft.agent_id || '').trim(); if (!agentId) return; const ok = await ui.confirmDialog({ title: t('subagentDeleteConfirmTitle'), message: t('subagentDeleteConfirmMessage', { id: agentId }), danger: true, confirmText: t('delete'), }); if (!ok) return; const delQ = `${q}${q ? '&' : '?'}agent_id=${encodeURIComponent(agentId)}`; const r = await fetch(`/webui/api/subagent_profiles${delQ}`, { method: 'DELETE' }); if (!r.ok) { await ui.notify({ title: t('requestFailed'), message: await r.text() }); return; } await load(); }; return (

{t('subagentProfiles')}

{t('subagentProfiles')}
{items.map((it) => ( ))} {items.length === 0 && (
No subagent profiles.
)}
{t('id')}
setDraft({ ...draft, agent_id: e.target.value })} className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded disabled:opacity-60" placeholder="coder" />
{t('name')}
setDraft({ ...draft, name: e.target.value })} className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" placeholder="Code Agent" />
Role
setDraft({ ...draft, role: e.target.value })} className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" placeholder="coding" />
{t('status')}
{t('memoryNamespace')}
setDraft({ ...draft, memory_namespace: e.target.value })} className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" placeholder="coder" />
{t('toolAllowlist')}
setDraft({ ...draft, tool_allowlist: parseAllowlist(e.target.value) })} className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" placeholder="read_file, list_files, memory_search" /> {groups.length > 0 && (
{groups.map((g) => ( ))}
)}
System Prompt