Add multi-agent config and registry runtime flow

This commit is contained in:
lpf
2026-03-06 12:47:05 +08:00
parent 959870e6f7
commit 6902f65c54
29 changed files with 4654 additions and 76 deletions

View File

@@ -20,6 +20,30 @@ const resources = {
subagentsRuntime: 'Subagents Runtime',
subagentDetail: 'Subagent Detail',
spawnSubagent: 'Spawn Subagent',
dispatchAndWait: 'Dispatch And Wait',
dispatchReply: 'Dispatch Reply',
mergedResult: 'Merged Result',
configSubagentDraft: 'Config Subagent Draft',
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.',
threadTrace: 'Thread Trace',
threadMessages: 'Thread Messages',
inbox: 'Inbox',
reply: 'Reply',
ack: 'Ack',
steerMessage: 'Steering message',
pipelines: 'Pipelines',
pipelineDetail: 'Pipeline Detail',
@@ -421,6 +445,30 @@ const resources = {
subagentsRuntime: '子代理运行态',
subagentDetail: '子代理详情',
spawnSubagent: '创建子代理任务',
dispatchAndWait: '派发并等待',
dispatchReply: '派发回复',
mergedResult: '汇总结果',
configSubagentDraft: '配置子代理草案',
agentRegistry: '代理注册表',
pendingSubagentDrafts: '待确认子代理草案',
subagentDraftDescription: '描述你希望主代理创建的子代理职责',
generateDraft: '生成草案',
confirmDraft: '确认草案',
loadDraft: '载入草案',
discardDraft: '丢弃草案',
enableAgent: '启用代理',
disableAgent: '停用代理',
deleteAgent: '删除代理',
deleteAgentConfirm: '确认从 config.json 中永久删除代理 "{{id}}" 吗?',
noRegistryAgents: '当前没有已配置代理。',
noPendingSubagentDrafts: '当前没有待确认的子代理草案。',
saveToConfig: '写入配置',
configSubagentSaved: '子代理配置已写入并刷新运行态。',
threadTrace: '线程追踪',
threadMessages: '线程消息',
inbox: '收件箱',
reply: '回复',
ack: '确认',
steerMessage: '引导消息',
pipelines: '流水线',
pipelineDetail: '流水线详情',

View File

@@ -22,6 +22,66 @@ type SubagentTask = {
updated?: number;
task?: string;
result?: string;
thread_id?: string;
correlation_id?: string;
waiting_for_reply?: boolean;
};
type RouterReply = {
task_id?: string;
thread_id?: string;
correlation_id?: string;
agent_id?: string;
status?: string;
result?: string;
};
type AgentThread = {
thread_id?: string;
owner?: string;
participants?: string[];
status?: string;
topic?: string;
};
type AgentMessage = {
message_id?: string;
thread_id?: string;
from_agent?: string;
to_agent?: string;
reply_to?: string;
correlation_id?: string;
type?: string;
content?: string;
requires_reply?: boolean;
status?: string;
created_at?: number;
};
type PendingSubagentDraft = {
session_key?: string;
draft?: {
agent_id?: string;
role?: string;
display_name?: string;
description?: string;
system_prompt?: string;
tool_allowlist?: string[];
routing_keywords?: string[];
};
};
type RegistrySubagent = {
agent_id?: string;
enabled?: boolean;
type?: string;
display_name?: string;
role?: string;
description?: string;
system_prompt?: string;
memory_namespace?: string;
tool_allowlist?: string[];
routing_keywords?: string[];
};
const Subagents: React.FC = () => {
@@ -36,21 +96,51 @@ const Subagents: React.FC = () => {
const [spawnRole, setSpawnRole] = useState('');
const [spawnLabel, setSpawnLabel] = useState('');
const [steerMessage, setSteerMessage] = useState('');
const [dispatchTask, setDispatchTask] = useState('');
const [dispatchAgentID, setDispatchAgentID] = useState('');
const [dispatchRole, setDispatchRole] = useState('');
const [dispatchWaitTimeout, setDispatchWaitTimeout] = useState('120');
const [dispatchReply, setDispatchReply] = useState<RouterReply | null>(null);
const [dispatchMerged, setDispatchMerged] = useState('');
const [threadDetail, setThreadDetail] = useState<AgentThread | null>(null);
const [threadMessages, setThreadMessages] = useState<AgentMessage[]>([]);
const [inboxMessages, setInboxMessages] = useState<AgentMessage[]>([]);
const [replyMessage, setReplyMessage] = useState('');
const [replyToMessageID, setReplyToMessageID] = useState('');
const [configAgentID, setConfigAgentID] = useState('');
const [configRole, setConfigRole] = useState('');
const [configDisplayName, setConfigDisplayName] = useState('');
const [configSystemPrompt, setConfigSystemPrompt] = useState('');
const [configToolAllowlist, setConfigToolAllowlist] = useState('');
const [configRoutingKeywords, setConfigRoutingKeywords] = useState('');
const [draftDescription, setDraftDescription] = useState('');
const [pendingDrafts, setPendingDrafts] = useState<PendingSubagentDraft[]>([]);
const [registryItems, setRegistryItems] = useState<RegistrySubagent[]>([]);
const apiPath = '/webui/api/subagents_runtime';
const withAction = (action: string) => `${apiPath}${q}${q ? '&' : '?'}action=${encodeURIComponent(action)}`;
const load = async () => {
const r = await fetch(withAction('list'));
if (!r.ok) throw new Error(await r.text());
const j = await r.json();
const [tasksRes, draftsRes, 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('');
return;
}
if (!arr.find((x: SubagentTask) => x.id === selectedId)) {
} else if (!arr.find((x: SubagentTask) => x.id === selectedId)) {
setSelectedId(arr[0].id || '');
}
};
@@ -74,6 +164,26 @@ const Subagents: React.FC = () => {
return r.json();
};
const loadThreadAndInbox = async (task: SubagentTask | null) => {
if (!task?.id) {
setThreadDetail(null);
setThreadMessages([]);
setInboxMessages([]);
return;
}
const [threadRes, inboxRes] = await Promise.all([
callAction({ action: 'thread', id: task.id, limit: 50 }),
callAction({ action: 'inbox', id: task.id, limit: 50 }),
]);
setThreadDetail(threadRes?.result?.thread || null);
setThreadMessages(Array.isArray(threadRes?.result?.messages) ? threadRes.result.messages : []);
setInboxMessages(Array.isArray(inboxRes?.result?.messages) ? inboxRes.result.messages : []);
};
useEffect(() => {
loadThreadAndInbox(selected).catch(() => {});
}, [selectedId, q, items]);
const spawn = async () => {
if (!spawnTask.trim()) {
await ui.notify({ title: t('requestFailed'), message: 'task is required' });
@@ -108,6 +218,158 @@ const Subagents: React.FC = () => {
await callAction({ action: 'steer', id: selected.id, message: steerMessage });
setSteerMessage('');
await load();
await loadThreadAndInbox(selected);
};
const dispatchAndWait = async () => {
if (!dispatchTask.trim()) {
await ui.notify({ title: t('requestFailed'), message: 'task is required' });
return;
}
const waitTimeout = Number.parseInt(dispatchWaitTimeout, 10);
const data = await callAction({
action: 'dispatch_and_wait',
task: dispatchTask,
agent_id: dispatchAgentID,
role: dispatchRole,
wait_timeout_sec: Number.isFinite(waitTimeout) && waitTimeout > 0 ? waitTimeout : 120,
});
if (!data) return;
setDispatchReply(data?.result?.reply || null);
setDispatchMerged(data?.result?.merged || '');
await load();
};
const sendMessage = async () => {
if (!selected?.id || !replyMessage.trim()) return;
await callAction({ action: 'send', id: selected.id, message: replyMessage });
setReplyMessage('');
setReplyToMessageID('');
await load();
await loadThreadAndInbox(selected);
};
const replyToMessage = async () => {
if (!selected?.id || !replyMessage.trim()) return;
await callAction({ action: 'reply', id: selected.id, message_id: replyToMessageID, message: replyMessage });
setReplyMessage('');
setReplyToMessageID('');
await load();
await loadThreadAndInbox(selected);
};
const ackMessage = async (messageID: string) => {
if (!selected?.id || !messageID) return;
await callAction({ action: 'ack', id: selected.id, message_id: messageID });
await load();
await loadThreadAndInbox(selected);
};
const upsertConfigSubagent = async () => {
if (!configAgentID.trim()) {
await ui.notify({ title: t('requestFailed'), message: 'agent_id is required' });
return;
}
const toolAllowlist = configToolAllowlist
.split(',')
.map((item) => item.trim())
.filter(Boolean);
const routingKeywords = configRoutingKeywords
.split(',')
.map((item) => item.trim())
.filter(Boolean);
const data = await callAction({
action: 'upsert_config_subagent',
agent_id: configAgentID,
role: configRole,
display_name: configDisplayName,
system_prompt: configSystemPrompt,
tool_allowlist: toolAllowlist,
routing_keywords: routingKeywords,
});
if (!data) return;
await ui.notify({ title: t('saved'), message: t('configSubagentSaved') });
setConfigAgentID('');
setConfigRole('');
setConfigDisplayName('');
setConfigSystemPrompt('');
setConfigToolAllowlist('');
setConfigRoutingKeywords('');
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 || '');
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 || '');
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 || '');
setConfigDisplayName(item.display_name || '');
setConfigSystemPrompt(item.system_prompt || '');
setConfigToolAllowlist(Array.isArray(item.tool_allowlist) ? item.tool_allowlist.join(', ') : '');
setConfigRoutingKeywords(Array.isArray(item.routing_keywords) ? item.routing_keywords.join(', ') : '');
};
const setRegistryEnabled = async (item: RegistrySubagent, enabled: boolean) => {
if (!item.agent_id) return;
const data = await callAction({ action: 'set_config_subagent_enabled', agent_id: item.agent_id, enabled });
if (!data) return;
await load();
};
const deleteRegistryItem = async (item: RegistrySubagent) => {
if (!item.agent_id) return;
const ok = await ui.confirmDialog({
title: t('deleteAgent'),
message: t('deleteAgentConfirm', { id: item.agent_id }),
danger: true,
confirmText: t('delete'),
});
if (!ok) return;
const data = await callAction({ action: 'delete_config_subagent', agent_id: item.agent_id });
if (!data) return;
await load();
};
return (
@@ -147,9 +409,12 @@ const Subagents: React.FC = () => {
<div><span className="text-zinc-400">Agent ID:</span> {selected.agent_id || '-'}</div>
<div><span className="text-zinc-400">Role:</span> {selected.role || '-'}</div>
<div className="md:col-span-2"><span className="text-zinc-400">Session:</span> {selected.session_key || '-'}</div>
<div className="md:col-span-2"><span className="text-zinc-400">Thread:</span> {selected.thread_id || '-'}</div>
<div className="md:col-span-2"><span className="text-zinc-400">Correlation:</span> {selected.correlation_id || '-'}</div>
<div className="md:col-span-2"><span className="text-zinc-400">Memory NS:</span> {selected.memory_ns || '-'}</div>
<div><span className="text-zinc-400">Retries:</span> {selected.retry_count || 0}/{selected.max_retries || 0}</div>
<div><span className="text-zinc-400">Timeout:</span> {selected.timeout_sec || 0}s</div>
<div><span className="text-zinc-400">Waiting Reply:</span> {selected.waiting_for_reply ? 'yes' : 'no'}</div>
</div>
<div className="text-xs text-zinc-400">Task</div>
<pre className="text-xs bg-zinc-950 border border-zinc-800 rounded p-3 whitespace-pre-wrap break-words">{selected.task || '-'}</pre>
@@ -187,6 +452,166 @@ const Subagents: React.FC = () => {
</div>
<button onClick={spawn} className="px-3 py-1.5 text-xs rounded bg-indigo-700/80 hover:bg-indigo-600">{t('spawn')}</button>
</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('agentRegistry')}</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">
{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-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' && (
<button onClick={() => deleteRegistryItem(item)} className="px-2 py-1 rounded bg-red-700/70 hover:bg-red-600 text-[11px]">{t('delete')}</button>
)}
</div>
</div>
))}
{registryItems.length === 0 && <div className="px-3 py-4 text-sm text-zinc-500">{t('noRegistryAgents')}</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('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" />
<input value={configDisplayName} onChange={(e) => setConfigDisplayName(e.target.value)} placeholder="display_name" className="px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
</div>
<textarea
value={configSystemPrompt}
onChange={(e) => setConfigSystemPrompt(e.target.value)}
placeholder="system_prompt"
className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded min-h-[96px]"
/>
<input value={configToolAllowlist} onChange={(e) => setConfigToolAllowlist(e.target.value)} placeholder="tool_allowlist (comma separated)" className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
<input value={configRoutingKeywords} onChange={(e) => setConfigRoutingKeywords(e.target.value)} placeholder="routing_keywords (comma separated)" className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
<button onClick={upsertConfigSubagent} className="px-3 py-1.5 text-xs rounded bg-amber-700/80 hover:bg-amber-600">{t('saveToConfig')}</button>
</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
value={dispatchTask}
onChange={(e) => setDispatchTask(e.target.value)}
placeholder="Task"
className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded min-h-[110px]"
/>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
<input value={dispatchAgentID} onChange={(e) => setDispatchAgentID(e.target.value)} placeholder="agent_id" className="px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
<input value={dispatchRole} onChange={(e) => setDispatchRole(e.target.value)} placeholder="role" className="px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
<input value={dispatchWaitTimeout} onChange={(e) => setDispatchWaitTimeout(e.target.value)} placeholder="wait_timeout_sec" className="px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
</div>
<button onClick={dispatchAndWait} className="px-3 py-1.5 text-xs rounded bg-emerald-700/80 hover:bg-emerald-600">{t('dispatchAndWait')}</button>
{dispatchReply && (
<>
<div className="text-xs text-zinc-400">{t('dispatchReply')}</div>
<pre className="text-xs bg-zinc-950 border border-zinc-800 rounded p-3 whitespace-pre-wrap break-words">{JSON.stringify(dispatchReply, null, 2)}</pre>
</>
)}
{dispatchMerged && (
<>
<div className="text-xs text-zinc-400">{t('mergedResult')}</div>
<pre className="text-xs bg-zinc-950 border border-zinc-800 rounded p-3 whitespace-pre-wrap break-words max-h-64 overflow-auto">{dispatchMerged}</pre>
</>
)}
</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('threadTrace')}</div>
{!selected && <div className="text-sm text-zinc-500">{t('selectTask')}</div>}
{selected && (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
<div><span className="text-zinc-400">Thread:</span> {threadDetail?.thread_id || selected.thread_id || '-'}</div>
<div><span className="text-zinc-400">Owner:</span> {threadDetail?.owner || '-'}</div>
<div><span className="text-zinc-400">Status:</span> {threadDetail?.status || '-'}</div>
<div><span className="text-zinc-400">Participants:</span> {(threadDetail?.participants || []).join(', ') || '-'}</div>
</div>
<div className="text-xs text-zinc-400">{t('threadMessages')}</div>
<div className="border border-zinc-800 rounded overflow-hidden">
{threadMessages.map((msg) => (
<div key={`${msg.message_id}-${msg.status}-${msg.created_at}`} className="px-3 py-2 border-b last:border-b-0 border-zinc-800/60 text-xs">
<div className="text-zinc-100">{msg.message_id} · {msg.type} · {msg.status}</div>
<div className="text-zinc-400">{msg.from_agent} {msg.to_agent} · reply_to: {msg.reply_to || '-'}</div>
<div className="text-zinc-300 mt-1 whitespace-pre-wrap break-words">{msg.content || '-'}</div>
</div>
))}
{threadMessages.length === 0 && <div className="px-3 py-4 text-sm text-zinc-500">No thread messages.</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('inbox')}</div>
{!selected && <div className="text-sm text-zinc-500">{t('selectTask')}</div>}
{selected && (
<>
<div className="border border-zinc-800 rounded overflow-hidden">
{inboxMessages.map((msg) => (
<div key={`${msg.message_id}-${msg.status}`} className="px-3 py-2 border-b last:border-b-0 border-zinc-800/60 text-xs">
<div className="flex items-center justify-between gap-3">
<div className="text-zinc-100">{msg.message_id} · {msg.type} · {msg.status}</div>
{msg.message_id && (
<button onClick={() => ackMessage(msg.message_id || '')} className="px-2 py-1 rounded bg-zinc-800 hover:bg-zinc-700 text-[11px]">{t('ack')}</button>
)}
</div>
<div className="text-zinc-400">{msg.from_agent} {msg.to_agent}</div>
<div className="text-zinc-300 mt-1 whitespace-pre-wrap break-words">{msg.content || '-'}</div>
</div>
))}
{inboxMessages.length === 0 && <div className="px-3 py-4 text-sm text-zinc-500">No queued inbox messages.</div>}
</div>
<div className="grid grid-cols-1 md:grid-cols-[1fr_220px] gap-2">
<input value={replyMessage} onChange={(e) => setReplyMessage(e.target.value)} placeholder={t('message')} className="px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
<input value={replyToMessageID} onChange={(e) => setReplyToMessageID(e.target.value)} placeholder="reply_to message_id" className="px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
</div>
<div className="flex items-center gap-2">
<button onClick={sendMessage} className="px-3 py-1.5 text-xs rounded bg-indigo-700/70 hover:bg-indigo-600">{t('send')}</button>
<button onClick={replyToMessage} className="px-3 py-1.5 text-xs rounded bg-emerald-700/70 hover:bg-emerald-600">{t('reply')}</button>
</div>
</>
)}
</div>
</div>
</div>
</div>