Release v0.1.0 agent topology and runtime refresh

This commit is contained in:
lpf
2026-03-06 17:44:13 +08:00
parent ac5a1bfcb2
commit 7d9ca89476
34 changed files with 1216 additions and 1462 deletions

View File

@@ -34,6 +34,7 @@ const Config: React.FC = () => {
);
const hotPrefixes = useMemo(() => hotReloadFieldDetails.map((x) => String(x.path || '').replace(/\.\*$/, '')).filter(Boolean), [hotReloadFieldDetails]);
const hotReloadTabKey = '__hot_reload__';
const allTopKeys = useMemo(() => Object.keys(cfg || {}).filter(k => typeof (cfg as any)?.[k] === 'object' && (cfg as any)?.[k] !== null), [cfg]);
const basicTopKeys = useMemo(() => {
@@ -50,7 +51,7 @@ const Config: React.FC = () => {
const s = search.trim().toLowerCase();
keys = keys.filter((k) => k.toLowerCase().includes(s));
}
return keys;
return [hotReloadTabKey, ...keys];
}, [allTopKeys, basicTopKeys, basicMode, hotOnly, search, hotPrefixes]);
const [selectedTop, setSelectedTop] = useState<string>('');
@@ -193,7 +194,7 @@ const Config: React.FC = () => {
}
return (
<div className="p-4 md:p-8 max-w-7xl mx-auto space-y-6 flex flex-col min-h-full">
<div className="p-4 md:p-8 w-full space-y-6 flex flex-col min-h-full">
<div className="flex items-center justify-between gap-3 flex-wrap">
<h1 className="text-2xl font-semibold tracking-tight">{t('configuration')}</h1>
<div className="flex items-center gap-1 bg-zinc-900/80 p-1 rounded-lg border border-zinc-800">
@@ -222,18 +223,6 @@ const Config: React.FC = () => {
</button>
</div>
<div className="bg-zinc-900/40 border border-zinc-800/80 rounded-2xl p-4">
<div className="text-sm font-semibold text-zinc-300 mb-2">{t('configHotFieldsFull')}</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs">
{hotReloadFieldDetails.map((it) => (
<div key={it.path} className="p-2 rounded bg-zinc-950 border border-zinc-800">
<div className="font-mono text-zinc-200">{it.path}</div>
<div className="text-zinc-400">{it.name || ''}{it.description ? ` · ${it.description}` : ''}</div>
</div>
))}
</div>
</div>
<div className="flex-1 bg-zinc-900/40 border border-zinc-800/80 rounded-2xl overflow-hidden flex flex-col shadow-sm min-h-[420px]">
{!showRaw ? (
<div className="flex-1 flex min-h-0">
@@ -246,13 +235,26 @@ const Config: React.FC = () => {
onClick={() => setSelectedTop(k)}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${activeTop === k ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30' : 'text-zinc-300 hover:bg-zinc-800/60'}`}
>
{configLabels[k] || k}
{k === hotReloadTabKey ? t('configHotFieldsFull') : (configLabels[k] || k)}
</button>
))}
</div>
</aside>
<div className="flex-1 p-4 md:p-6 overflow-y-auto space-y-4">
{activeTop === hotReloadTabKey && (
<div className="space-y-3">
<div className="text-sm font-semibold text-zinc-300">{t('configHotFieldsFull')}</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs">
{hotReloadFieldDetails.map((it) => (
<div key={it.path} className="p-2 rounded bg-zinc-950 border border-zinc-800">
<div className="font-mono text-zinc-200">{it.path}</div>
<div className="text-zinc-400">{it.name || ''}{it.description ? ` · ${it.description}` : ''}</div>
</div>
))}
</div>
</div>
)}
{activeTop === 'providers' && !showRaw && (
<div className="rounded-xl border border-zinc-800 bg-zinc-950/40 p-3 space-y-3">
<div className="flex items-center justify-between gap-2 flex-wrap">
@@ -278,7 +280,7 @@ const Config: React.FC = () => {
</div>
</div>
)}
{activeTop ? (
{activeTop && activeTop !== hotReloadTabKey ? (
<RecursiveConfig
data={(cfg as any)?.[activeTop] || {}}
labels={configLabels}

View File

@@ -8,6 +8,7 @@ type SubagentProfile = {
name?: string;
role?: string;
system_prompt?: string;
system_prompt_file?: string;
tool_allowlist?: string[];
memory_namespace?: string;
max_retries?: number;
@@ -32,6 +33,7 @@ const emptyDraft: SubagentProfile = {
name: '',
role: '',
system_prompt: '',
system_prompt_file: '',
memory_namespace: '',
status: 'active',
tool_allowlist: [],
@@ -52,6 +54,8 @@ const SubagentProfiles: React.FC = () => {
const [draft, setDraft] = useState<SubagentProfile>(emptyDraft);
const [saving, setSaving] = useState(false);
const [groups, setGroups] = useState<ToolAllowlistGroup[]>([]);
const [promptFileContent, setPromptFileContent] = useState('');
const [promptFileFound, setPromptFileFound] = useState(false);
const selected = useMemo(
() => items.find((p) => p.agent_id === selectedId) || null,
@@ -77,6 +81,7 @@ const SubagentProfiles: React.FC = () => {
name: next.name || '',
role: next.role || '',
system_prompt: next.system_prompt || '',
system_prompt_file: next.system_prompt_file || '',
memory_namespace: next.memory_namespace || '',
status: (next.status as string) || 'active',
tool_allowlist: Array.isArray(next.tool_allowlist) ? next.tool_allowlist : [],
@@ -103,6 +108,33 @@ const SubagentProfiles: React.FC = () => {
loadGroups().catch(() => {});
}, [q]);
useEffect(() => {
const path = String(draft.system_prompt_file || '').trim();
if (!path) {
setPromptFileContent('');
setPromptFileFound(false);
return;
}
fetch(`/webui/api/subagents_runtime${q}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'prompt_file_get', path }),
})
.then(async (r) => {
if (!r.ok) throw new Error(await r.text());
return r.json();
})
.then((data) => {
const found = data?.result?.found === true;
setPromptFileFound(found);
setPromptFileContent(found ? String(data?.result?.content || '') : '');
})
.catch(() => {
setPromptFileFound(false);
setPromptFileContent('');
});
}, [draft.system_prompt_file, q]);
const onSelect = (p: SubagentProfile) => {
setSelectedId(p.agent_id || '');
setDraft({
@@ -110,6 +142,7 @@ const SubagentProfiles: React.FC = () => {
name: p.name || '',
role: p.role || '',
system_prompt: p.system_prompt || '',
system_prompt_file: p.system_prompt_file || '',
memory_namespace: p.memory_namespace || '',
status: (p.status as string) || 'active',
tool_allowlist: Array.isArray(p.tool_allowlist) ? p.tool_allowlist : [],
@@ -162,6 +195,7 @@ const SubagentProfiles: React.FC = () => {
name: draft.name || '',
role: draft.role || '',
system_prompt: draft.system_prompt || '',
system_prompt_file: draft.system_prompt_file || '',
memory_namespace: draft.memory_namespace || '',
status: draft.status || 'active',
tool_allowlist: draft.tool_allowlist || [],
@@ -218,6 +252,25 @@ const SubagentProfiles: React.FC = () => {
await load();
};
const savePromptFile = async () => {
const path = String(draft.system_prompt_file || '').trim();
if (!path) {
await ui.notify({ title: t('requestFailed'), message: 'system_prompt_file is required' });
return;
}
const r = await fetch(`/webui/api/subagents_runtime${q}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'prompt_file_set', path, content: promptFileContent }),
});
if (!r.ok) {
await ui.notify({ title: t('requestFailed'), message: await r.text() });
return;
}
setPromptFileFound(true);
await ui.notify({ title: t('saved'), message: t('promptFileSaved') });
};
return (
<div className="h-full p-4 md:p-6 flex flex-col gap-4">
<div className="flex items-center justify-between">
@@ -297,6 +350,15 @@ const SubagentProfiles: React.FC = () => {
<option value="disabled">disabled</option>
</select>
</div>
<div className="md:col-span-2">
<div className="text-xs text-zinc-400 mb-1">system_prompt_file</div>
<input
value={draft.system_prompt_file || ''}
onChange={(e) => setDraft({ ...draft, system_prompt_file: e.target.value })}
className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded"
placeholder="agents/coder/AGENT.md"
/>
</div>
<div className="md:col-span-2">
<div className="text-xs text-zinc-400 mb-1">{t('memoryNamespace')}</div>
<input
@@ -339,6 +401,28 @@ const SubagentProfiles: React.FC = () => {
placeholder="You are a coding specialist..."
/>
</div>
<div className="md:col-span-2">
<div className="flex items-center justify-between mb-1 gap-3">
<div className="text-xs text-zinc-400">system_prompt_file content</div>
<div className="text-[11px] text-zinc-500">{promptFileFound ? t('promptFileReady') : t('promptFileMissing')}</div>
</div>
<textarea
value={promptFileContent}
onChange={(e) => setPromptFileContent(e.target.value)}
className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded min-h-[220px]"
placeholder="AGENT.md content..."
/>
<div className="mt-2 flex items-center gap-2">
<button
type="button"
onClick={savePromptFile}
disabled={!String(draft.system_prompt_file || '').trim()}
className="px-3 py-1.5 text-xs rounded bg-zinc-800 hover:bg-zinc-700 disabled:opacity-50"
>
{t('savePromptFile')}
</button>
</div>
</div>
<div>
<div className="text-xs text-zinc-400 mb-1">Max Retries</div>
<input

File diff suppressed because it is too large Load Diff