mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-14 22:09:37 +08:00
Unify agent topology and subagent memory logging
This commit is contained in:
@@ -7,7 +7,6 @@ import Dashboard from './pages/Dashboard';
|
||||
import Chat from './pages/Chat';
|
||||
import Config from './pages/Config';
|
||||
import Cron from './pages/Cron';
|
||||
import Nodes from './pages/Nodes';
|
||||
import Logs from './pages/Logs';
|
||||
import Skills from './pages/Skills';
|
||||
import Memory from './pages/Memory';
|
||||
@@ -32,7 +31,6 @@ export default function App() {
|
||||
<Route path="skills" element={<Skills />} />
|
||||
<Route path="config" element={<Config />} />
|
||||
<Route path="cron" element={<Cron />} />
|
||||
<Route path="nodes" element={<Nodes />} />
|
||||
<Route path="memory" element={<Memory />} />
|
||||
<Route path="task-audit" element={<TaskAudit />} />
|
||||
<Route path="ekg" element={<EKG />} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { LayoutDashboard, MessageSquare, Settings, Clock, Server, Terminal, Zap, FolderOpen, ClipboardList, BrainCircuit, Hash, Bot, Workflow, Boxes } from 'lucide-react';
|
||||
import { LayoutDashboard, MessageSquare, Settings, Clock, Terminal, Zap, FolderOpen, ClipboardList, BrainCircuit, Hash, Bot, Workflow, Boxes } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppContext } from '../context/AppContext';
|
||||
import NavItem from './NavItem';
|
||||
@@ -14,6 +14,7 @@ const Sidebar: React.FC = () => {
|
||||
items: [
|
||||
{ icon: <LayoutDashboard className="w-5 h-5" />, label: t('dashboard'), to: '/' },
|
||||
{ icon: <MessageSquare className="w-5 h-5" />, label: t('chat'), to: '/chat' },
|
||||
{ icon: <Boxes className="w-5 h-5" />, label: t('subagentsRuntime'), to: '/subagents' },
|
||||
{ icon: <Terminal className="w-5 h-5" />, label: t('logs'), to: '/logs' },
|
||||
{ icon: <Hash className="w-5 h-5" />, label: t('logCodes'), to: '/log-codes' },
|
||||
{ icon: <Zap className="w-5 h-5" />, label: t('skills'), to: '/skills' },
|
||||
@@ -24,10 +25,8 @@ const Sidebar: React.FC = () => {
|
||||
items: [
|
||||
{ icon: <Settings className="w-5 h-5" />, label: t('config'), to: '/config' },
|
||||
{ icon: <Clock className="w-5 h-5" />, label: t('cronJobs'), to: '/cron' },
|
||||
{ icon: <Server className="w-5 h-5" />, label: t('nodes'), to: '/nodes' },
|
||||
{ icon: <FolderOpen className="w-5 h-5" />, label: t('memory'), to: '/memory' },
|
||||
{ icon: <Bot className="w-5 h-5" />, label: t('subagentProfiles'), to: '/subagent-profiles' },
|
||||
{ icon: <Boxes className="w-5 h-5" />, label: t('subagentsRuntime'), to: '/subagents' },
|
||||
{ icon: <Workflow className="w-5 h-5" />, label: t('pipelines'), to: '/pipelines' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -14,6 +14,8 @@ interface AppContextType {
|
||||
setCfgRaw: (raw: string) => void;
|
||||
nodes: string;
|
||||
setNodes: (nodes: string) => void;
|
||||
nodeTrees: string;
|
||||
setNodeTrees: (trees: string) => void;
|
||||
cron: CronJob[];
|
||||
setCron: (cron: CronJob[]) => void;
|
||||
skills: Skill[];
|
||||
@@ -53,6 +55,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const [cfg, setCfg] = useState<Cfg>({});
|
||||
const [cfgRaw, setCfgRaw] = useState('{}');
|
||||
const [nodes, setNodes] = useState('[]');
|
||||
const [nodeTrees, setNodeTrees] = useState('[]');
|
||||
const [cron, setCron] = useState<CronJob[]>([]);
|
||||
const [skills, setSkills] = useState<Skill[]>([]);
|
||||
const [clawhubInstalled, setClawhubInstalled] = useState(false);
|
||||
@@ -99,6 +102,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
if (!r.ok) throw new Error('Failed to load nodes');
|
||||
const j = await r.json();
|
||||
setNodes(JSON.stringify(j.nodes || [], null, 2));
|
||||
setNodeTrees(JSON.stringify(j.trees || [], null, 2));
|
||||
setIsGatewayOnline(true);
|
||||
} catch (e) {
|
||||
setIsGatewayOnline(false);
|
||||
@@ -167,6 +171,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
useEffect(() => {
|
||||
refreshAll();
|
||||
const interval = setInterval(() => {
|
||||
loadConfig();
|
||||
refreshCron();
|
||||
refreshNodes();
|
||||
refreshSkills();
|
||||
@@ -174,12 +179,12 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
refreshVersion();
|
||||
}, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, [token, refreshAll, refreshCron, refreshNodes, refreshSkills, refreshSessions, refreshVersion]);
|
||||
}, [token, refreshAll, loadConfig, refreshCron, refreshNodes, refreshSkills, refreshSessions, refreshVersion]);
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{
|
||||
token, setToken, sidebarOpen, setSidebarOpen, isGatewayOnline, setIsGatewayOnline,
|
||||
cfg, setCfg, cfgRaw, setCfgRaw, nodes, setNodes,
|
||||
cfg, setCfg, cfgRaw, setCfgRaw, nodes, setNodes, nodeTrees, setNodeTrees,
|
||||
cron, setCron, skills, setSkills, clawhubInstalled, clawhubPath,
|
||||
sessions, setSessions,
|
||||
refreshAll, refreshCron, refreshNodes, refreshSkills, refreshSessions, refreshVersion, loadConfig,
|
||||
|
||||
@@ -10,6 +10,10 @@ const resources = {
|
||||
config: 'Config',
|
||||
cronJobs: 'Cron Jobs',
|
||||
nodes: 'Nodes',
|
||||
agentTree: 'Agent Tree',
|
||||
noAgentTree: 'No agent tree available.',
|
||||
readonlyMirror: 'Read-only mirror',
|
||||
localControl: 'Local control',
|
||||
logs: 'Real-time Logs',
|
||||
logCodes: 'Log Codes',
|
||||
skills: 'Skills',
|
||||
@@ -17,26 +21,32 @@ const resources = {
|
||||
taskAudit: 'Task Audit',
|
||||
tasks: 'Tasks',
|
||||
subagentProfiles: 'Subagent Profiles',
|
||||
subagentsRuntime: 'Subagents Runtime',
|
||||
subagentsRuntime: 'Agents',
|
||||
agentTopology: 'Agent Topology',
|
||||
agentTopologyHint: 'Unified graph for local agents, registered nodes, and mirrored remote agent branches.',
|
||||
runningTasks: 'running',
|
||||
clearFocus: 'Clear Focus',
|
||||
childrenCount: 'children',
|
||||
'topologyFilter.all': 'All',
|
||||
'topologyFilter.running': 'Running',
|
||||
'topologyFilter.failed': 'Failed',
|
||||
'topologyFilter.local': 'Local',
|
||||
'topologyFilter.remote': 'Remote',
|
||||
noLiveTasks: 'No live tasks',
|
||||
remoteTasksUnavailable: 'Remote task details are not mirrored yet.',
|
||||
subagentDetail: 'Subagent Detail',
|
||||
spawnSubagent: 'Spawn Subagent',
|
||||
dispatchAndWait: 'Dispatch And Wait',
|
||||
dispatchReply: 'Dispatch Reply',
|
||||
mergedResult: 'Merged Result',
|
||||
configSubagentDraft: 'Config Subagent Draft',
|
||||
configSubagentDraft: 'Config Subagent',
|
||||
agentRegistry: 'Agent Registry',
|
||||
pendingSubagentDrafts: 'Pending Subagent Drafts',
|
||||
subagentDraftDescription: 'Describe the subagent you want the main agent to create',
|
||||
generateDraft: 'Generate Draft',
|
||||
confirmDraft: 'Confirm Draft',
|
||||
loadDraft: 'Load Draft',
|
||||
discardDraft: 'Discard Draft',
|
||||
enableAgent: 'Enable Agent',
|
||||
disableAgent: 'Disable Agent',
|
||||
deleteAgent: 'Delete Agent',
|
||||
deleteAgentConfirm: 'Delete agent "{{id}}" from config.json permanently?',
|
||||
noRegistryAgents: 'No configured agents.',
|
||||
noPendingSubagentDrafts: 'No pending subagent drafts.',
|
||||
saveToConfig: 'Save To Config',
|
||||
configSubagentSaved: 'Subagent config saved and runtime updated.',
|
||||
promptFileEditor: 'Prompt File Editor',
|
||||
@@ -455,6 +465,10 @@ const resources = {
|
||||
config: '配置',
|
||||
cronJobs: '定时任务',
|
||||
nodes: '节点',
|
||||
agentTree: '代理树',
|
||||
noAgentTree: '当前没有可用的代理树。',
|
||||
readonlyMirror: '只读镜像',
|
||||
localControl: '本地控制',
|
||||
logs: '实时日志',
|
||||
logCodes: '日志编号',
|
||||
skills: '技能管理',
|
||||
@@ -462,26 +476,32 @@ const resources = {
|
||||
taskAudit: '任务审计',
|
||||
tasks: '任务管理',
|
||||
subagentProfiles: '子代理档案',
|
||||
subagentsRuntime: '子代理运行态',
|
||||
subagentsRuntime: 'Agents',
|
||||
agentTopology: 'Agent 拓扑',
|
||||
agentTopologyHint: '统一展示本地 agent、注册 node 以及远端镜像 agent 分支的关系图。',
|
||||
runningTasks: '运行中',
|
||||
clearFocus: '清除聚焦',
|
||||
childrenCount: '子节点',
|
||||
'topologyFilter.all': '全部',
|
||||
'topologyFilter.running': '运行中',
|
||||
'topologyFilter.failed': '失败',
|
||||
'topologyFilter.local': '本地',
|
||||
'topologyFilter.remote': '远端',
|
||||
noLiveTasks: '当前没有活动任务',
|
||||
remoteTasksUnavailable: '远端任务细节暂未镜像回来。',
|
||||
subagentDetail: '子代理详情',
|
||||
spawnSubagent: '创建子代理任务',
|
||||
dispatchAndWait: '派发并等待',
|
||||
dispatchReply: '派发回复',
|
||||
mergedResult: '汇总结果',
|
||||
configSubagentDraft: '配置子代理草案',
|
||||
configSubagentDraft: '配置子代理',
|
||||
agentRegistry: '代理注册表',
|
||||
pendingSubagentDrafts: '待确认子代理草案',
|
||||
subagentDraftDescription: '描述你希望主代理创建的子代理职责',
|
||||
generateDraft: '生成草案',
|
||||
confirmDraft: '确认草案',
|
||||
loadDraft: '载入草案',
|
||||
discardDraft: '丢弃草案',
|
||||
loadDraft: '载入配置',
|
||||
enableAgent: '启用代理',
|
||||
disableAgent: '停用代理',
|
||||
deleteAgent: '删除代理',
|
||||
deleteAgentConfirm: '确认从 config.json 中永久删除代理 "{{id}}" 吗?',
|
||||
noRegistryAgents: '当前没有已配置代理。',
|
||||
noPendingSubagentDrafts: '当前没有待确认的子代理草案。',
|
||||
saveToConfig: '写入配置',
|
||||
configSubagentSaved: '子代理配置已写入并刷新运行态。',
|
||||
promptFileEditor: '提示词文件编辑器',
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from 'react';
|
||||
import { RefreshCw, Server } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppContext } from '../context/AppContext';
|
||||
|
||||
const Nodes: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { nodes, refreshNodes } = useAppContext();
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto space-y-8 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{t('nodes')}</h1>
|
||||
<button onClick={refreshNodes} className="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-sm font-medium transition-colors">
|
||||
<RefreshCw className="w-4 h-4" /> {t('refresh')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 bg-zinc-900/40 border border-zinc-800/80 rounded-2xl p-6 flex flex-col shadow-sm overflow-hidden">
|
||||
<div className="flex-1 bg-zinc-950/80 rounded-xl border border-zinc-800/50 p-4 overflow-auto">
|
||||
{(() => {
|
||||
try {
|
||||
const parsedNodes = JSON.parse(nodes);
|
||||
if (!Array.isArray(parsedNodes) || parsedNodes.length === 0) {
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center justify-center text-zinc-500 space-y-4">
|
||||
<Server className="w-12 h-12 opacity-20" />
|
||||
<p className="text-lg font-medium">{t('noNodes')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{parsedNodes.map((node: any, i: number) => (
|
||||
<div key={node.id || i} className="flex items-center justify-between p-4 bg-zinc-900/50 rounded-xl border border-zinc-800/50 hover:border-zinc-700/50 transition-colors">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-3 h-3 rounded-full ${node.online ? 'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]' : 'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.5)]'}`} />
|
||||
<div>
|
||||
<div className="font-semibold text-zinc-200">{node.name || node.id || `${t('node')} ${i + 1}`}</div>
|
||||
<div className="text-xs text-zinc-500 font-mono mt-0.5">{node.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs font-mono text-zinc-400 bg-zinc-800/50 px-2 py-1 rounded">{node.ip || t('unknownIp')}</div>
|
||||
<div className="text-[10px] text-zinc-600 mt-1 font-mono uppercase tracking-widest">v{node.version || '0.0.0'}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} catch (e) {
|
||||
return <pre className="font-mono text-[13px] leading-relaxed text-zinc-400 whitespace-pre-wrap">{nodes}</pre>;
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Nodes;
|
||||
@@ -58,24 +58,14 @@ type AgentMessage = {
|
||||
created_at?: number;
|
||||
};
|
||||
|
||||
type PendingSubagentDraft = {
|
||||
session_key?: string;
|
||||
draft?: {
|
||||
agent_id?: string;
|
||||
role?: string;
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
system_prompt?: string;
|
||||
system_prompt_file?: string;
|
||||
tool_allowlist?: string[];
|
||||
routing_keywords?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
type RegistrySubagent = {
|
||||
agent_id?: string;
|
||||
enabled?: boolean;
|
||||
type?: string;
|
||||
transport?: string;
|
||||
node_id?: string;
|
||||
parent_agent_id?: string;
|
||||
managed_by?: string;
|
||||
display_name?: string;
|
||||
role?: string;
|
||||
description?: string;
|
||||
@@ -87,9 +77,165 @@ type RegistrySubagent = {
|
||||
routing_keywords?: string[];
|
||||
};
|
||||
|
||||
type AgentTreeNode = {
|
||||
agent_id?: string;
|
||||
display_name?: string;
|
||||
role?: string;
|
||||
type?: string;
|
||||
transport?: string;
|
||||
managed_by?: string;
|
||||
node_id?: string;
|
||||
enabled?: boolean;
|
||||
children?: AgentTreeNode[];
|
||||
};
|
||||
|
||||
type NodeTree = {
|
||||
node_id?: string;
|
||||
node_name?: string;
|
||||
online?: boolean;
|
||||
source?: string;
|
||||
readonly?: boolean;
|
||||
root?: {
|
||||
root?: AgentTreeNode;
|
||||
};
|
||||
};
|
||||
|
||||
type NodeInfo = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
endpoint?: string;
|
||||
version?: string;
|
||||
online?: boolean;
|
||||
};
|
||||
|
||||
type AgentTaskStats = {
|
||||
total: number;
|
||||
running: number;
|
||||
failed: number;
|
||||
waiting: number;
|
||||
active: Array<{ id: string; status: string; title: string }>;
|
||||
};
|
||||
|
||||
type GraphCardSpec = {
|
||||
key: string;
|
||||
branch: string;
|
||||
agentID?: string;
|
||||
transportType?: 'local' | 'remote';
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
kind: 'node' | 'agent';
|
||||
title: string;
|
||||
subtitle: string;
|
||||
meta: string[];
|
||||
accent: string;
|
||||
online?: boolean;
|
||||
clickable?: boolean;
|
||||
highlighted?: boolean;
|
||||
dimmed?: boolean;
|
||||
hidden?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type GraphLineSpec = {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
dashed?: boolean;
|
||||
branch: string;
|
||||
highlighted?: boolean;
|
||||
dimmed?: boolean;
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
const cardWidth = 230;
|
||||
const cardHeight = 112;
|
||||
const clusterWidth = 350;
|
||||
const topY = 24;
|
||||
const mainY = 172;
|
||||
const childStartY = 334;
|
||||
const childGap = 132;
|
||||
|
||||
function normalizeTitle(value?: string, fallback = '-'): string {
|
||||
const trimmed = `${value || ''}`.trim();
|
||||
return trimmed || fallback;
|
||||
}
|
||||
|
||||
function summarizeTask(task?: string, label?: string): string {
|
||||
const text = normalizeTitle(label || task, '-');
|
||||
return text.length > 52 ? `${text.slice(0, 49)}...` : text;
|
||||
}
|
||||
|
||||
function buildTaskStats(tasks: SubagentTask[]): Record<string, AgentTaskStats> {
|
||||
return tasks.reduce<Record<string, AgentTaskStats>>((acc, task) => {
|
||||
const agentID = normalizeTitle(task.agent_id, '');
|
||||
if (!agentID) return acc;
|
||||
if (!acc[agentID]) {
|
||||
acc[agentID] = { total: 0, running: 0, failed: 0, waiting: 0, active: [] };
|
||||
}
|
||||
const item = acc[agentID];
|
||||
item.total += 1;
|
||||
if (task.status === 'running') item.running += 1;
|
||||
if (task.status === 'failed') item.failed += 1;
|
||||
if (task.waiting_for_reply) item.waiting += 1;
|
||||
if (task.status === 'running' || task.waiting_for_reply) {
|
||||
item.active.push({
|
||||
id: task.id,
|
||||
status: task.status || '-',
|
||||
title: summarizeTask(task.task, task.label),
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function GraphCard({ card }: { card: GraphCardSpec }) {
|
||||
return (
|
||||
<foreignObject x={card.x} y={card.y} width={card.w} height={card.h}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={card.onClick}
|
||||
disabled={!card.clickable}
|
||||
className={`h-full w-full rounded-2xl border text-left p-3 shadow-sm ${
|
||||
card.clickable ? 'cursor-pointer hover:border-zinc-600' : 'cursor-default'
|
||||
}`}
|
||||
style={{
|
||||
background: card.highlighted ? 'rgba(39,39,42,0.98)' : 'rgba(24,24,27,0.92)',
|
||||
borderColor: card.highlighted ? 'rgba(251,191,36,0.85)' : 'rgba(63,63,70,0.9)',
|
||||
boxShadow: card.highlighted
|
||||
? '0 0 0 1px rgba(251,191,36,0.35), 0 12px 30px rgba(0,0,0,0.28)'
|
||||
: card.online
|
||||
? '0 0 0 1px rgba(16,185,129,0.15)'
|
||||
: undefined,
|
||||
opacity: card.dimmed ? 0.38 : 1,
|
||||
transform: card.highlighted ? 'translateY(-2px)' : undefined,
|
||||
transition: 'all 160ms ease',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-semibold text-zinc-100">{card.title}</div>
|
||||
<div className="truncate text-[11px] text-zinc-500">{card.subtitle}</div>
|
||||
</div>
|
||||
<div className={`h-2.5 w-2.5 rounded-full ${card.accent}`} />
|
||||
</div>
|
||||
<div className="mt-3 space-y-1">
|
||||
{card.meta.slice(0, 4).map((line, idx) => (
|
||||
<div key={`${card.key}-${idx}`} className="truncate text-[11px] text-zinc-300">
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
</foreignObject>
|
||||
);
|
||||
}
|
||||
|
||||
const Subagents: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { q } = useAppContext();
|
||||
const { q, nodeTrees, nodes } = useAppContext();
|
||||
const ui = useUI();
|
||||
|
||||
const [items, setItems] = useState<SubagentTask[]>([]);
|
||||
@@ -117,32 +263,27 @@ const Subagents: React.FC = () => {
|
||||
const [configSystemPromptFile, setConfigSystemPromptFile] = useState('');
|
||||
const [configToolAllowlist, setConfigToolAllowlist] = useState('');
|
||||
const [configRoutingKeywords, setConfigRoutingKeywords] = useState('');
|
||||
const [draftDescription, setDraftDescription] = useState('');
|
||||
const [pendingDrafts, setPendingDrafts] = useState<PendingSubagentDraft[]>([]);
|
||||
const [registryItems, setRegistryItems] = useState<RegistrySubagent[]>([]);
|
||||
const [promptFileContent, setPromptFileContent] = useState('');
|
||||
const [promptFileFound, setPromptFileFound] = useState(false);
|
||||
const [selectedTopologyBranch, setSelectedTopologyBranch] = useState('');
|
||||
const [topologyFilter, setTopologyFilter] = useState<'all' | 'running' | 'failed' | 'local' | 'remote'>('all');
|
||||
|
||||
const apiPath = '/webui/api/subagents_runtime';
|
||||
const withAction = (action: string) => `${apiPath}${q}${q ? '&' : '?'}action=${encodeURIComponent(action)}`;
|
||||
|
||||
const load = async () => {
|
||||
const [tasksRes, draftsRes, registryRes] = await Promise.all([
|
||||
const [tasksRes, registryRes] = await Promise.all([
|
||||
fetch(withAction('list')),
|
||||
fetch(withAction('pending_drafts')),
|
||||
fetch(withAction('registry')),
|
||||
]);
|
||||
if (!tasksRes.ok) throw new Error(await tasksRes.text());
|
||||
if (!draftsRes.ok) throw new Error(await draftsRes.text());
|
||||
if (!registryRes.ok) throw new Error(await registryRes.text());
|
||||
const j = await tasksRes.json();
|
||||
const draftsJson = await draftsRes.json();
|
||||
const registryJson = await registryRes.json();
|
||||
const arr = Array.isArray(j?.result?.items) ? j.result.items : [];
|
||||
const draftItems = Array.isArray(draftsJson?.result?.items) ? draftsJson.result.items : [];
|
||||
const registryItems = Array.isArray(registryJson?.result?.items) ? registryJson.result.items : [];
|
||||
setItems(arr);
|
||||
setPendingDrafts(draftItems);
|
||||
setRegistryItems(registryItems);
|
||||
if (arr.length === 0) {
|
||||
setSelectedId('');
|
||||
@@ -155,7 +296,318 @@ const Subagents: React.FC = () => {
|
||||
load().catch(() => {});
|
||||
}, [q]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = window.setInterval(() => {
|
||||
load().catch(() => {});
|
||||
}, 5000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [q]);
|
||||
|
||||
const selected = useMemo(() => items.find((x) => x.id === selectedId) || null, [items, selectedId]);
|
||||
const parsedNodeTrees = useMemo<NodeTree[]>(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(nodeTrees);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}, [nodeTrees]);
|
||||
const parsedNodes = useMemo<NodeInfo[]>(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(nodes);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}, [nodes]);
|
||||
const taskStats = useMemo(() => buildTaskStats(items), [items]);
|
||||
const recentTaskByAgent = useMemo(() => {
|
||||
return items.reduce<Record<string, SubagentTask>>((acc, task) => {
|
||||
const agentID = normalizeTitle(task.agent_id, '');
|
||||
if (!agentID) return acc;
|
||||
const existing = acc[agentID];
|
||||
const currentScore = Math.max(task.updated || 0, task.created || 0);
|
||||
const existingScore = existing ? Math.max(existing.updated || 0, existing.created || 0) : -1;
|
||||
if (!existing || currentScore > existingScore) {
|
||||
acc[agentID] = task;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}, [items]);
|
||||
const topologyGraph = useMemo(() => {
|
||||
const localTree = parsedNodeTrees.find((tree) => normalizeTitle(tree.node_id, '') === 'local') || null;
|
||||
const remoteTrees = parsedNodeTrees.filter((tree) => normalizeTitle(tree.node_id, '') !== 'local');
|
||||
const localRoot = localTree?.root?.root || {
|
||||
agent_id: 'main',
|
||||
display_name: 'Main Agent',
|
||||
role: 'orchestrator',
|
||||
type: 'router',
|
||||
transport: 'local',
|
||||
enabled: true,
|
||||
children: registryItems
|
||||
.filter((item) => item.agent_id && item.agent_id !== 'main' && item.managed_by === 'config.json')
|
||||
.map((item) => ({
|
||||
agent_id: item.agent_id,
|
||||
display_name: item.display_name,
|
||||
role: item.role,
|
||||
type: item.type,
|
||||
transport: item.transport,
|
||||
enabled: item.enabled,
|
||||
children: [],
|
||||
})),
|
||||
};
|
||||
const localNode = parsedNodes.find((node) => normalizeTitle(node.id, '') === 'local') || {
|
||||
id: 'local',
|
||||
name: 'local',
|
||||
online: true,
|
||||
};
|
||||
const clusterCount = Math.max(1, remoteTrees.length + 1);
|
||||
const localChildren = Array.isArray(localRoot.children) ? localRoot.children : [];
|
||||
const remoteChildMax = remoteTrees.reduce((max, tree) => {
|
||||
const count = Array.isArray(tree.root?.root?.children) ? tree.root?.root?.children?.length || 0 : 0;
|
||||
return Math.max(max, count);
|
||||
}, 0);
|
||||
const maxChildren = Math.max(localChildren.length, remoteChildMax, 1);
|
||||
const width = clusterCount * clusterWidth + 120;
|
||||
const height = childStartY + maxChildren * childGap + 30;
|
||||
const mainClusterX = 56;
|
||||
const localNodeX = mainClusterX + 20;
|
||||
const localMainX = mainClusterX + 20;
|
||||
const localMainCenterX = localMainX + cardWidth / 2;
|
||||
const localMainCenterY = mainY + cardHeight / 2;
|
||||
const cards: GraphCardSpec[] = [];
|
||||
const lines: GraphLineSpec[] = [];
|
||||
const localBranch = 'local';
|
||||
const localBranchStats = {
|
||||
running: 0,
|
||||
failed: 0,
|
||||
};
|
||||
|
||||
const localNodeCard: GraphCardSpec = {
|
||||
key: 'node-local',
|
||||
branch: localBranch,
|
||||
transportType: 'local',
|
||||
kind: 'node',
|
||||
x: localNodeX,
|
||||
y: topY,
|
||||
w: cardWidth,
|
||||
h: cardHeight,
|
||||
title: normalizeTitle(localNode.name, 'local'),
|
||||
subtitle: normalizeTitle(localNode.id, 'local'),
|
||||
meta: [
|
||||
localNode.online ? t('online') : t('offline'),
|
||||
localNode.endpoint ? `endpoint=${localNode.endpoint}` : 'endpoint=-',
|
||||
localNode.version ? `version=${localNode.version}` : 'version=-',
|
||||
`${t('childrenCount')}=${localChildren.length}`,
|
||||
],
|
||||
accent: localNode.online ? 'bg-emerald-500' : 'bg-red-500',
|
||||
online: !!localNode.online,
|
||||
clickable: true,
|
||||
onClick: () => setSelectedTopologyBranch(localBranch),
|
||||
};
|
||||
const localMainStats = taskStats[normalizeTitle(localRoot.agent_id, 'main')] || { total: 0, running: 0, failed: 0, waiting: 0, active: [] };
|
||||
const localMainTask = recentTaskByAgent[normalizeTitle(localRoot.agent_id, 'main')];
|
||||
localBranchStats.running += localMainStats.running;
|
||||
localBranchStats.failed += localMainStats.failed;
|
||||
const localMainCard: GraphCardSpec = {
|
||||
key: 'agent-main',
|
||||
branch: localBranch,
|
||||
agentID: normalizeTitle(localRoot.agent_id, 'main'),
|
||||
transportType: 'local',
|
||||
kind: 'agent',
|
||||
x: localMainX,
|
||||
y: mainY,
|
||||
w: cardWidth,
|
||||
h: cardHeight,
|
||||
title: normalizeTitle(localRoot.display_name, 'Main Agent'),
|
||||
subtitle: `${normalizeTitle(localRoot.agent_id, 'main')} · ${normalizeTitle(localRoot.role, '-')}`,
|
||||
meta: [
|
||||
`total=${localMainStats.total} running=${localMainStats.running}`,
|
||||
`waiting=${localMainStats.waiting} failed=${localMainStats.failed}`,
|
||||
`transport=${normalizeTitle(localRoot.transport, 'local')} type=${normalizeTitle(localRoot.type, 'router')}`,
|
||||
localMainStats.active[0] ? `task: ${localMainStats.active[0].title}` : t('noLiveTasks'),
|
||||
],
|
||||
accent: 'bg-amber-400',
|
||||
clickable: true,
|
||||
onClick: () => {
|
||||
setSelectedTopologyBranch(localBranch);
|
||||
if (localMainTask?.id) setSelectedId(localMainTask.id);
|
||||
},
|
||||
};
|
||||
cards.push(localNodeCard, localMainCard);
|
||||
lines.push({
|
||||
x1: localNodeCard.x + cardWidth / 2,
|
||||
y1: localNodeCard.y + cardHeight,
|
||||
x2: localMainCard.x + cardWidth / 2,
|
||||
y2: localMainCard.y,
|
||||
branch: localBranch,
|
||||
});
|
||||
|
||||
localChildren.forEach((child, idx) => {
|
||||
const childX = mainClusterX + 20;
|
||||
const childY = childStartY + idx * childGap;
|
||||
const stats = taskStats[normalizeTitle(child.agent_id, '')] || { total: 0, running: 0, failed: 0, waiting: 0, active: [] };
|
||||
const task = recentTaskByAgent[normalizeTitle(child.agent_id, '')];
|
||||
localBranchStats.running += stats.running;
|
||||
localBranchStats.failed += stats.failed;
|
||||
cards.push({
|
||||
key: `local-child-${child.agent_id || idx}`,
|
||||
branch: localBranch,
|
||||
agentID: normalizeTitle(child.agent_id, ''),
|
||||
transportType: 'local',
|
||||
kind: 'agent',
|
||||
x: childX,
|
||||
y: childY,
|
||||
w: cardWidth,
|
||||
h: cardHeight,
|
||||
title: normalizeTitle(child.display_name, normalizeTitle(child.agent_id, 'agent')),
|
||||
subtitle: `${normalizeTitle(child.agent_id, '-')} · ${normalizeTitle(child.role, '-')}`,
|
||||
meta: [
|
||||
`total=${stats.total} running=${stats.running}`,
|
||||
`waiting=${stats.waiting} failed=${stats.failed}`,
|
||||
`transport=${normalizeTitle(child.transport, 'local')} type=${normalizeTitle(child.type, 'worker')}`,
|
||||
stats.active[0] ? `task: ${stats.active[0].title}` : task ? `last: ${summarizeTask(task.task, task.label)}` : t('noLiveTasks'),
|
||||
],
|
||||
accent: stats.running > 0 ? 'bg-emerald-500' : stats.failed > 0 ? 'bg-red-500' : 'bg-sky-400',
|
||||
clickable: true,
|
||||
onClick: () => {
|
||||
setSelectedTopologyBranch(localBranch);
|
||||
if (task?.id) setSelectedId(task.id);
|
||||
},
|
||||
});
|
||||
lines.push({
|
||||
x1: localMainCenterX,
|
||||
y1: localMainCenterY,
|
||||
x2: childX + cardWidth / 2,
|
||||
y2: childY,
|
||||
branch: localBranch,
|
||||
});
|
||||
});
|
||||
|
||||
remoteTrees.forEach((tree, treeIndex) => {
|
||||
const branch = `node:${normalizeTitle(tree.node_id, `remote-${treeIndex}`)}`;
|
||||
const baseX = 56 + (treeIndex + 1) * clusterWidth;
|
||||
const nodeX = baseX + 20;
|
||||
const rootX = baseX + 20;
|
||||
const treeRoot = tree.root?.root;
|
||||
const remoteNodeCard: GraphCardSpec = {
|
||||
key: `node-${tree.node_id || treeIndex}`,
|
||||
branch,
|
||||
transportType: 'remote',
|
||||
kind: 'node',
|
||||
x: nodeX,
|
||||
y: topY,
|
||||
w: cardWidth,
|
||||
h: cardHeight,
|
||||
title: normalizeTitle(tree.node_name, tree.node_id || 'node'),
|
||||
subtitle: normalizeTitle(tree.node_id, 'node'),
|
||||
meta: [
|
||||
tree.online ? t('online') : t('offline'),
|
||||
normalizeTitle(tree.source, '-'),
|
||||
tree.readonly ? t('readonlyMirror') : t('localControl'),
|
||||
`${t('childrenCount')}=${Array.isArray(treeRoot?.children) ? treeRoot?.children?.length || 0 : 0}`,
|
||||
],
|
||||
accent: tree.online ? 'bg-emerald-500' : 'bg-red-500',
|
||||
online: !!tree.online,
|
||||
clickable: true,
|
||||
onClick: () => setSelectedTopologyBranch(branch),
|
||||
};
|
||||
cards.push(remoteNodeCard);
|
||||
lines.push({
|
||||
x1: localMainCenterX,
|
||||
y1: localMainCenterY,
|
||||
x2: remoteNodeCard.x,
|
||||
y2: remoteNodeCard.y + cardHeight / 2,
|
||||
dashed: true,
|
||||
branch,
|
||||
});
|
||||
if (!treeRoot) return;
|
||||
const rootCard: GraphCardSpec = {
|
||||
key: `remote-root-${tree.node_id || treeIndex}`,
|
||||
branch,
|
||||
agentID: normalizeTitle(treeRoot.agent_id, ''),
|
||||
transportType: 'remote',
|
||||
kind: 'agent',
|
||||
x: rootX,
|
||||
y: mainY,
|
||||
w: cardWidth,
|
||||
h: cardHeight,
|
||||
title: normalizeTitle(treeRoot.display_name, treeRoot.agent_id || 'main'),
|
||||
subtitle: `${normalizeTitle(treeRoot.agent_id, '-')} · ${normalizeTitle(treeRoot.role, '-')}`,
|
||||
meta: [
|
||||
`transport=${normalizeTitle(treeRoot.transport, 'node')} type=${normalizeTitle(treeRoot.type, 'router')}`,
|
||||
`source=${normalizeTitle(treeRoot.managed_by, tree.source || '-')}`,
|
||||
t('remoteTasksUnavailable'),
|
||||
],
|
||||
accent: tree.online ? 'bg-fuchsia-400' : 'bg-zinc-500',
|
||||
clickable: true,
|
||||
onClick: () => setSelectedTopologyBranch(branch),
|
||||
};
|
||||
cards.push(rootCard);
|
||||
lines.push({
|
||||
x1: remoteNodeCard.x + cardWidth / 2,
|
||||
y1: remoteNodeCard.y + cardHeight,
|
||||
x2: rootCard.x + cardWidth / 2,
|
||||
y2: rootCard.y,
|
||||
branch,
|
||||
});
|
||||
const children = Array.isArray(treeRoot.children) ? treeRoot.children : [];
|
||||
children.forEach((child, idx) => {
|
||||
const childX = baseX + 20;
|
||||
const childY = childStartY + idx * childGap;
|
||||
cards.push({
|
||||
key: `remote-child-${tree.node_id || treeIndex}-${child.agent_id || idx}`,
|
||||
branch,
|
||||
agentID: normalizeTitle(child.agent_id, ''),
|
||||
transportType: 'remote',
|
||||
kind: 'agent',
|
||||
x: childX,
|
||||
y: childY,
|
||||
w: cardWidth,
|
||||
h: cardHeight,
|
||||
title: normalizeTitle(child.display_name, child.agent_id || 'agent'),
|
||||
subtitle: `${normalizeTitle(child.agent_id, '-')} · ${normalizeTitle(child.role, '-')}`,
|
||||
meta: [
|
||||
`transport=${normalizeTitle(child.transport, 'node')} type=${normalizeTitle(child.type, 'worker')}`,
|
||||
`source=${normalizeTitle(child.managed_by, 'remote_webui')}`,
|
||||
t('remoteTasksUnavailable'),
|
||||
],
|
||||
accent: 'bg-violet-400',
|
||||
clickable: true,
|
||||
onClick: () => setSelectedTopologyBranch(branch),
|
||||
});
|
||||
lines.push({
|
||||
x1: rootCard.x + cardWidth / 2,
|
||||
y1: rootCard.y + cardHeight / 2,
|
||||
x2: childX + cardWidth / 2,
|
||||
y2: childY,
|
||||
branch,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const highlightedBranch = selectedTopologyBranch.trim();
|
||||
const branchFilters = new Map<string, boolean>();
|
||||
branchFilters.set(localBranch, topologyFilter === 'all' || topologyFilter === 'local' || (topologyFilter === 'running' && localBranchStats.running > 0) || (topologyFilter === 'failed' && localBranchStats.failed > 0));
|
||||
remoteTrees.forEach((tree, treeIndex) => {
|
||||
const branch = `node:${normalizeTitle(tree.node_id, `remote-${treeIndex}`)}`;
|
||||
branchFilters.set(branch, topologyFilter === 'all' || topologyFilter === 'remote');
|
||||
});
|
||||
const decoratedCards = cards.map((card) => ({
|
||||
...card,
|
||||
hidden: branchFilters.get(card.branch) === false,
|
||||
highlighted: !highlightedBranch || card.branch === highlightedBranch,
|
||||
dimmed: branchFilters.get(card.branch) === false ? true : !!highlightedBranch && card.branch !== highlightedBranch,
|
||||
}));
|
||||
const decoratedLines = lines.map((line) => ({
|
||||
...line,
|
||||
hidden: branchFilters.get(line.branch) === false,
|
||||
highlighted: !highlightedBranch || line.branch === highlightedBranch,
|
||||
dimmed: branchFilters.get(line.branch) === false ? true : !!highlightedBranch && line.branch !== highlightedBranch,
|
||||
}));
|
||||
|
||||
return { width, height, cards: decoratedCards, lines: decoratedLines };
|
||||
}, [parsedNodeTrees, parsedNodes, registryItems, taskStats, recentTaskByAgent, selectedTopologyBranch, topologyFilter, t]);
|
||||
|
||||
const callAction = async (payload: Record<string, any>) => {
|
||||
const r = await fetch(`${apiPath}${q}`, {
|
||||
@@ -322,52 +774,6 @@ const Subagents: React.FC = () => {
|
||||
await load();
|
||||
};
|
||||
|
||||
const draftConfigSubagent = async () => {
|
||||
if (!draftDescription.trim()) {
|
||||
await ui.notify({ title: t('requestFailed'), message: 'description is required' });
|
||||
return;
|
||||
}
|
||||
const data = await callAction({
|
||||
action: 'draft_config_subagent',
|
||||
description: draftDescription,
|
||||
agent_id_hint: configAgentID,
|
||||
});
|
||||
if (!data) return;
|
||||
const draft = data?.result?.draft || {};
|
||||
setConfigAgentID(draft.agent_id || '');
|
||||
setConfigRole(draft.role || '');
|
||||
setConfigDisplayName(draft.display_name || '');
|
||||
setConfigSystemPrompt(draft.system_prompt || '');
|
||||
setConfigSystemPromptFile(draft.system_prompt_file || '');
|
||||
setConfigToolAllowlist(Array.isArray(draft.tool_allowlist) ? draft.tool_allowlist.join(', ') : '');
|
||||
setConfigRoutingKeywords(Array.isArray(draft.routing_keywords) ? draft.routing_keywords.join(', ') : '');
|
||||
await load();
|
||||
};
|
||||
|
||||
const fillDraftForm = (draft: PendingSubagentDraft['draft']) => {
|
||||
if (!draft) return;
|
||||
setConfigAgentID(draft.agent_id || '');
|
||||
setConfigRole(draft.role || '');
|
||||
setConfigDisplayName(draft.display_name || '');
|
||||
setConfigSystemPrompt(draft.system_prompt || '');
|
||||
setConfigSystemPromptFile(draft.system_prompt_file || '');
|
||||
setConfigToolAllowlist(Array.isArray(draft.tool_allowlist) ? draft.tool_allowlist.join(', ') : '');
|
||||
setConfigRoutingKeywords(Array.isArray(draft.routing_keywords) ? draft.routing_keywords.join(', ') : '');
|
||||
};
|
||||
|
||||
const clearPendingDraft = async (sessionKey: string) => {
|
||||
const data = await callAction({ action: 'clear_pending_draft', session_key: sessionKey });
|
||||
if (!data) return;
|
||||
await load();
|
||||
};
|
||||
|
||||
const confirmPendingDraft = async (sessionKey: string) => {
|
||||
const data = await callAction({ action: 'confirm_pending_draft', session_key: sessionKey });
|
||||
if (!data) return;
|
||||
await ui.notify({ title: t('saved'), message: data?.result?.message || t('configSubagentSaved') });
|
||||
await load();
|
||||
};
|
||||
|
||||
const loadRegistryItem = (item: RegistrySubagent) => {
|
||||
setConfigAgentID(item.agent_id || '');
|
||||
setConfigRole(item.role || '');
|
||||
@@ -443,6 +849,61 @@ const Subagents: React.FC = () => {
|
||||
<button onClick={() => load()} className="px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-sm">{t('refresh')}</button>
|
||||
</div>
|
||||
|
||||
<div className="border border-zinc-800 rounded-2xl bg-zinc-900/40 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-xs text-zinc-400 uppercase tracking-wider">{t('agentTopology')}</div>
|
||||
<div className="text-sm text-zinc-500">{t('agentTopologyHint')}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap justify-end">
|
||||
{(['all', 'running', 'failed', 'local', 'remote'] as const).map((filter) => (
|
||||
<button
|
||||
key={filter}
|
||||
onClick={() => setTopologyFilter(filter)}
|
||||
className={`px-2 py-1 rounded text-[11px] ${
|
||||
topologyFilter === filter ? 'bg-amber-500/20 text-amber-200 border border-amber-500/40' : 'bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700'
|
||||
}`}
|
||||
>
|
||||
{t(`topologyFilter.${filter}`)}
|
||||
</button>
|
||||
))}
|
||||
{selectedTopologyBranch && (
|
||||
<button
|
||||
onClick={() => setSelectedTopologyBranch('')}
|
||||
className="px-2 py-1 rounded bg-zinc-800 hover:bg-zinc-700 text-[11px] text-zinc-200"
|
||||
>
|
||||
{t('clearFocus')}
|
||||
</button>
|
||||
)}
|
||||
<div className="text-xs text-zinc-500">
|
||||
{items.filter((item) => item.status === 'running').length} {t('runningTasks')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto overflow-y-hidden rounded-xl border border-zinc-800 bg-[radial-gradient(circle_at_top,_rgba(251,191,36,0.08),_transparent_24%),linear-gradient(180deg,rgba(24,24,27,0.95),rgba(9,9,11,0.98))]">
|
||||
<svg width={topologyGraph.width} height={topologyGraph.height} className="block">
|
||||
{topologyGraph.lines.map((line, idx) => (
|
||||
line.hidden ? null : (
|
||||
<line
|
||||
key={`line-${idx}`}
|
||||
x1={line.x1}
|
||||
y1={line.y1}
|
||||
x2={line.x2}
|
||||
y2={line.y2}
|
||||
stroke={line.highlighted ? 'rgba(251,191,36,0.9)' : line.dashed ? 'rgba(251,191,36,0.5)' : 'rgba(113,113,122,0.7)'}
|
||||
strokeWidth={line.highlighted ? '2.8' : '2'}
|
||||
strokeDasharray={line.dashed ? '6 6' : undefined}
|
||||
opacity={line.dimmed ? 0.22 : 1}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
{topologyGraph.cards.map((card) => (
|
||||
card.hidden ? null : <GraphCard key={card.key} card={card} />
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 grid grid-cols-1 xl:grid-cols-[360px_1fr] gap-4">
|
||||
<div className="border border-zinc-800 rounded-xl bg-zinc-900/40 overflow-hidden">
|
||||
<div className="px-3 py-2 border-b border-zinc-800 text-xs text-zinc-400 uppercase tracking-wider">{t('subagentsRuntime')}</div>
|
||||
@@ -526,17 +987,28 @@ const Subagents: React.FC = () => {
|
||||
{registryItems.map((item) => (
|
||||
<div key={item.agent_id || 'unknown'} className="px-3 py-2 border-b last:border-b-0 border-zinc-800/60 text-xs space-y-2">
|
||||
<div className="text-zinc-100">{item.agent_id || '-'} · {item.role || '-'} · {item.enabled ? t('active') : t('paused')}</div>
|
||||
<div className="text-zinc-400">{item.type || '-'} · {item.display_name || '-'}</div>
|
||||
<div className="text-zinc-400">{item.type || '-'} · {item.transport || 'local'} · {item.display_name || '-'}</div>
|
||||
{(item.node_id || item.parent_agent_id || item.managed_by) && (
|
||||
<div className="text-zinc-500 break-words">
|
||||
{item.node_id ? `node=${item.node_id}` : ''}
|
||||
{item.node_id && item.parent_agent_id ? ' · ' : ''}
|
||||
{item.parent_agent_id ? `parent=${item.parent_agent_id}` : ''}
|
||||
{(item.node_id || item.parent_agent_id) && item.managed_by ? ' · ' : ''}
|
||||
{item.managed_by ? `source=${item.managed_by}` : ''}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-zinc-500 break-words">{item.system_prompt_file || '-'}</div>
|
||||
<div className="text-zinc-500">{item.prompt_file_found ? t('promptFileReady') : t('promptFileMissing')}</div>
|
||||
<div className="text-zinc-300 whitespace-pre-wrap break-words">{item.system_prompt || item.description || '-'}</div>
|
||||
<div className="text-zinc-500 break-words">{(item.routing_keywords || []).join(', ') || '-'}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => loadRegistryItem(item)} className="px-2 py-1 rounded bg-indigo-700/70 hover:bg-indigo-600 text-[11px]">{t('loadDraft')}</button>
|
||||
<button onClick={() => setRegistryEnabled(item, !item.enabled)} className="px-2 py-1 rounded bg-zinc-800 hover:bg-zinc-700 text-[11px]">
|
||||
{item.enabled ? t('disableAgent') : t('enableAgent')}
|
||||
</button>
|
||||
{item.agent_id !== 'main' && (
|
||||
{item.managed_by === 'config.json' && (
|
||||
<button onClick={() => setRegistryEnabled(item, !item.enabled)} className="px-2 py-1 rounded bg-zinc-800 hover:bg-zinc-700 text-[11px]">
|
||||
{item.enabled ? t('disableAgent') : t('enableAgent')}
|
||||
</button>
|
||||
)}
|
||||
{item.managed_by === 'config.json' && item.agent_id !== 'main' && (
|
||||
<button onClick={() => deleteRegistryItem(item)} className="px-2 py-1 rounded bg-red-700/70 hover:bg-red-600 text-[11px]">{t('delete')}</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -548,13 +1020,6 @@ const Subagents: React.FC = () => {
|
||||
|
||||
<div className="border border-zinc-800 rounded-xl bg-zinc-900/40 p-4 space-y-3">
|
||||
<div className="text-xs text-zinc-400 uppercase tracking-wider">{t('configSubagentDraft')}</div>
|
||||
<textarea
|
||||
value={draftDescription}
|
||||
onChange={(e) => setDraftDescription(e.target.value)}
|
||||
placeholder={t('subagentDraftDescription')}
|
||||
className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded min-h-[90px]"
|
||||
/>
|
||||
<button onClick={draftConfigSubagent} className="px-3 py-1.5 text-xs rounded bg-zinc-700 hover:bg-zinc-600">{t('generateDraft')}</button>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||
<input value={configAgentID} onChange={(e) => setConfigAgentID(e.target.value)} placeholder="agent_id" className="px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
|
||||
<input value={configRole} onChange={(e) => setConfigRole(e.target.value)} placeholder="role" className="px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
|
||||
@@ -595,28 +1060,6 @@ const Subagents: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-zinc-800 rounded-xl bg-zinc-900/40 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-xs text-zinc-400 uppercase tracking-wider">{t('pendingSubagentDrafts')}</div>
|
||||
<button onClick={() => load()} className="px-2 py-1 text-[11px] rounded bg-zinc-800 hover:bg-zinc-700">{t('refresh')}</button>
|
||||
</div>
|
||||
<div className="border border-zinc-800 rounded overflow-hidden">
|
||||
{pendingDrafts.map((item) => (
|
||||
<div key={item.session_key || 'unknown'} className="px-3 py-2 border-b last:border-b-0 border-zinc-800/60 text-xs space-y-2">
|
||||
<div className="text-zinc-100">{item.draft?.agent_id || '-'} · {item.draft?.role || '-'}</div>
|
||||
<div className="text-zinc-400">session: {item.session_key || '-'}</div>
|
||||
<div className="text-zinc-300 whitespace-pre-wrap break-words">{item.draft?.system_prompt || item.draft?.description || '-'}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => fillDraftForm(item.draft)} className="px-2 py-1 rounded bg-indigo-700/70 hover:bg-indigo-600 text-[11px]">{t('loadDraft')}</button>
|
||||
<button onClick={() => confirmPendingDraft(item.session_key || 'main')} className="px-2 py-1 rounded bg-emerald-700/70 hover:bg-emerald-600 text-[11px]">{t('confirmDraft')}</button>
|
||||
<button onClick={() => clearPendingDraft(item.session_key || 'main')} className="px-2 py-1 rounded bg-zinc-800 hover:bg-zinc-700 text-[11px]">{t('discardDraft')}</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{pendingDrafts.length === 0 && <div className="px-3 py-4 text-sm text-zinc-500">{t('noPendingSubagentDrafts')}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-zinc-800 rounded-xl bg-zinc-900/40 p-4 space-y-3">
|
||||
<div className="text-xs text-zinc-400 uppercase tracking-wider">{t('dispatchAndWait')}</div>
|
||||
<textarea
|
||||
|
||||
@@ -19,7 +19,7 @@ export type CronJob = {
|
||||
to?: string;
|
||||
};
|
||||
export type Cfg = Record<string, any>;
|
||||
export type View = 'dashboard' | 'chat' | 'config' | 'cron' | 'nodes' | 'memory';
|
||||
export type View = 'dashboard' | 'chat' | 'config' | 'cron' | 'memory';
|
||||
export type Lang = 'en' | 'zh';
|
||||
|
||||
export type LogEntry = {
|
||||
|
||||
Reference in New Issue
Block a user