Tighten subagent prompt file workflow

This commit is contained in:
lpf
2026-03-06 14:12:01 +08:00
parent 49352612ea
commit 5e421bb730
15 changed files with 542 additions and 58 deletions

View File

@@ -39,6 +39,14 @@ const resources = {
noPendingSubagentDrafts: 'No pending subagent drafts.',
saveToConfig: 'Save To Config',
configSubagentSaved: 'Subagent config saved and runtime updated.',
promptFileEditor: 'Prompt File Editor',
promptFileEditorPlaceholder: 'Edit the AGENT.md content for this subagent.',
bootstrapPromptFile: 'Bootstrap AGENT.md',
savePromptFile: 'Save AGENT.md',
promptFileSaved: 'Prompt file saved.',
promptFileBootstrapped: 'Prompt file template created.',
promptFileReady: 'AGENT.md ready',
promptFileMissing: 'AGENT.md missing',
threadTrace: 'Thread Trace',
threadMessages: 'Thread Messages',
inbox: 'Inbox',
@@ -465,6 +473,14 @@ const resources = {
noPendingSubagentDrafts: '当前没有待确认的子代理草案。',
saveToConfig: '写入配置',
configSubagentSaved: '子代理配置已写入并刷新运行态。',
promptFileEditor: '提示词文件编辑器',
promptFileEditorPlaceholder: '编辑该子代理对应的 AGENT.md 内容。',
bootstrapPromptFile: '生成 AGENT.md 模板',
savePromptFile: '保存 AGENT.md',
promptFileSaved: '提示词文件已保存。',
promptFileBootstrapped: '提示词模板已创建。',
promptFileReady: 'AGENT.md 已就绪',
promptFileMissing: 'AGENT.md 缺失',
threadTrace: '线程追踪',
threadMessages: '线程消息',
inbox: '收件箱',

View File

@@ -81,6 +81,7 @@ type RegistrySubagent = {
description?: string;
system_prompt?: string;
system_prompt_file?: string;
prompt_file_found?: boolean;
memory_namespace?: string;
tool_allowlist?: string[];
routing_keywords?: string[];
@@ -119,6 +120,8 @@ const Subagents: React.FC = () => {
const [draftDescription, setDraftDescription] = useState('');
const [pendingDrafts, setPendingDrafts] = useState<PendingSubagentDraft[]>([]);
const [registryItems, setRegistryItems] = useState<RegistrySubagent[]>([]);
const [promptFileContent, setPromptFileContent] = useState('');
const [promptFileFound, setPromptFileFound] = useState(false);
const apiPath = '/webui/api/subagents_runtime';
const withAction = (action: string) => `${apiPath}${q}${q ? '&' : '?'}action=${encodeURIComponent(action)}`;
@@ -187,6 +190,22 @@ const Subagents: React.FC = () => {
loadThreadAndInbox(selected).catch(() => {});
}, [selectedId, q, items]);
useEffect(() => {
const path = configSystemPromptFile.trim();
if (!path) {
setPromptFileContent('');
setPromptFileFound(false);
return;
}
callAction({ action: 'prompt_file_get', path })
.then((data) => {
const found = data?.result?.found === true;
setPromptFileFound(found);
setPromptFileContent(found ? data?.result?.content || '' : '');
})
.catch(() => {});
}, [configSystemPromptFile, q]);
const spawn = async () => {
if (!spawnTask.trim()) {
await ui.notify({ title: t('requestFailed'), message: 'task is required' });
@@ -331,6 +350,7 @@ const Subagents: React.FC = () => {
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(', ') : '');
};
@@ -353,7 +373,7 @@ const Subagents: React.FC = () => {
setConfigRole(item.role || '');
setConfigDisplayName(item.display_name || '');
setConfigSystemPrompt(item.system_prompt || '');
setConfigSystemPromptFile((item as any).system_prompt_file || '');
setConfigSystemPromptFile(item.system_prompt_file || '');
setConfigToolAllowlist(Array.isArray(item.tool_allowlist) ? item.tool_allowlist.join(', ') : '');
setConfigRoutingKeywords(Array.isArray(item.routing_keywords) ? item.routing_keywords.join(', ') : '');
};
@@ -379,6 +399,43 @@ const Subagents: React.FC = () => {
await load();
};
const savePromptFile = async () => {
if (!configSystemPromptFile.trim()) {
await ui.notify({ title: t('requestFailed'), message: 'system_prompt_file is required' });
return;
}
const data = await callAction({
action: 'prompt_file_set',
path: configSystemPromptFile,
content: promptFileContent,
});
if (!data) return;
setPromptFileFound(true);
await ui.notify({ title: t('saved'), message: t('promptFileSaved') });
await load();
};
const bootstrapPromptFile = async () => {
if (!configAgentID.trim()) {
await ui.notify({ title: t('requestFailed'), message: 'agent_id is required' });
return;
}
const data = await callAction({
action: 'prompt_file_bootstrap',
agent_id: configAgentID,
role: configRole,
display_name: configDisplayName,
path: configSystemPromptFile,
});
if (!data) return;
const path = data?.result?.path || configSystemPromptFile || `agents/${configAgentID}/AGENT.md`;
setConfigSystemPromptFile(path);
setPromptFileFound(true);
setPromptFileContent(data?.result?.content || '');
await ui.notify({ title: t('saved'), message: t('promptFileBootstrapped') });
await load();
};
return (
<div className="h-full p-4 md:p-6 flex flex-col gap-4">
<div className="flex items-center justify-between">
@@ -470,6 +527,8 @@ const Subagents: React.FC = () => {
<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-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">
@@ -513,6 +572,29 @@ const Subagents: React.FC = () => {
<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('promptFileEditor')}</div>
<div className="text-[11px] text-zinc-500">{promptFileFound ? t('promptFileReady') : t('promptFileMissing')}</div>
</div>
<input
value={configSystemPromptFile}
onChange={(e) => setConfigSystemPromptFile(e.target.value)}
placeholder="agents/<agent_id>/AGENT.md"
className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded"
/>
<textarea
value={promptFileContent}
onChange={(e) => setPromptFileContent(e.target.value)}
placeholder={t('promptFileEditorPlaceholder')}
className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded min-h-[220px]"
/>
<div className="flex items-center gap-2">
<button onClick={bootstrapPromptFile} className="px-3 py-1.5 text-xs rounded bg-zinc-700 hover:bg-zinc-600">{t('bootstrapPromptFile')}</button>
<button onClick={savePromptFile} className="px-3 py-1.5 text-xs rounded bg-emerald-700/80 hover:bg-emerald-600">{t('savePromptFile')}</button>
</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>