feat: add sub-agent hot reload to settings, polish definition editor

- rename settings tab and editor labels (子代理定义 → 子代理, 可继续委托给 → 递归委托)
- show available sub-agents as selectable tags when delegation is a list
- fix edit button text wrapping next to the enable switch
- add hot reload button + phase status polling to the sub-agent pane
This commit is contained in:
xiaoxixi 2026-08-13 18:26:23 +08:00
parent b13450498b
commit 7de7a40de7
6 changed files with 68 additions and 14 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "picobot"
version = "1.13.0"
version = "1.13.1"
edition = "2024"
[dependencies]

View File

@ -1,12 +1,12 @@
{
"name": "picobot-webui",
"version": "1.13.0",
"version": "1.13.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picobot-webui",
"version": "1.13.0",
"version": "1.13.1",
"dependencies": {
"bits-ui": "^2.0.0",
"dompurify": "^3.4.12",

View File

@ -1,7 +1,7 @@
{
"name": "picobot-webui",
"private": true,
"version": "1.13.0",
"version": "1.13.1",
"type": "module",
"engines": {
"node": ">=20"

View File

@ -10,8 +10,23 @@
let error = $state("");
let editing = $state(null);
let saving = $state(false);
let reloading = $state(false);
let reloadStatus = $state(null);
let pollTimer = null;
let { notify } = $props();
const phaseBadge = $derived.by(() => {
if (!reloadStatus) return "ok";
const map = { active: "ok", preparing: "run", draining: "run", activating: "run", failed: "fail" };
return map[reloadStatus.phase] || "ok";
});
const phaseLabel = $derived.by(() => {
if (!reloadStatus) return "";
const map = { active: "运行中", preparing: "准备中", draining: "等待任务完成", activating: "激活中", failed: "失败" };
return map[reloadStatus.phase] || reloadStatus.phase;
});
const blank = () => ({
id: "",
description: "",
@ -44,6 +59,33 @@
}
}
async function reload() {
reloading = true;
try {
const result = await api("/api/config/reload", { method: "POST" });
notify(result.message || "已触发热重载");
await pollReloadStatus();
} catch (caught) {
notify(caught.message, true);
} finally {
reloading = false;
}
}
async function pollReloadStatus() {
try { reloadStatus = await api("/api/config/reload/status"); } catch {}
}
function startPolling() {
stopPolling();
pollReloadStatus();
pollTimer = setInterval(pollReloadStatus, 3000);
}
function stopPolling() {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
}
function toggleTool(list, name) {
const i = list.indexOf(name);
if (i >= 0) list.splice(i, 1);
@ -132,7 +174,7 @@
else if (editing.delegateMode === "list") payload.delegates = editing.delegates;
await api("/api/agents", { method: "POST", body: JSON.stringify(payload) });
editing = null;
notify("子代理已保存(需重载配置生效)");
notify("子代理已保存,点击「热重载」使其生效");
await load();
} catch (caught) {
notify(caught.message, true);
@ -181,7 +223,11 @@
return tool?.description || "";
}
onMount(load);
onMount(() => {
load();
startPolling();
return stopPolling;
});
</script>
<div class="definitions">
@ -189,10 +235,16 @@
<div>
<h2 style="margin:0">具名子代理</h2>
<p style="margin:2px 0 0;color:var(--muted);font-size:12px">
子代理由 <code>~/.picobot/agents/*.md</code> 定义工具、Skill、Provider 与模型在此直接指定。主 Agent 可委托给任意子代理。改动需热重载后生效。
子代理由 <code>~/.picobot/agents/*.md</code> 定义工具、Skill、Provider 与模型在此直接指定。保存后点击「热重载」使改动生效。
</p>
</div>
<button class="primary" onclick={startNew}><Icon name="add" size={16} />新增子代理</button>
<div class="toolbar-actions">
{#if reloadStatus}
<span class="badge {phaseBadge}" title={reloadStatus.last_error || ""}>{phaseLabel}</span>
{/if}
<button class="secondary" onclick={reload} disabled={reloading}><Icon name="refresh" size={15} />{reloading ? "重载中…" : "热重载"}</button>
<button class="primary" onclick={startNew}><Icon name="add" size={16} />新增子代理</button>
</div>
</div>
{#if loading}
@ -239,7 +291,7 @@
<button type="button" class="modal-scrim" onclick={cancelEdit} aria-label="关闭" tabindex="-1"></button>
<div class="modal" role="dialog" aria-label="编辑子代理">
<div class="editor-head">
<div><strong>{editing.id ? `编辑 ${editing.id}` : "新增子代理"}</strong><small>保存后需热重载配置生效</small></div>
<div><strong>{editing.id ? `编辑 ${editing.id}` : "新增子代理"}</strong><small>保存后点击「热重载」使改动生效</small></div>
<button class="icon-button" aria-label="关闭" onclick={cancelEdit}><Icon name="dismiss" size={18} /></button>
</div>
<div class="agent-form">
@ -288,7 +340,7 @@
{/each}
</div>
<div class="form-label">可继续委托给 <small>(该子代理可再委托给谁</small></div>
<div class="form-label">递归委托 <small>(该子代理可再委托给哪些子代理</small></div>
<select bind:value={editing.delegateMode}>
<option value="default">默认:仅 general-purpose</option>
<option value="none">不可继续委托</option>
@ -298,7 +350,7 @@
{#if editing.delegateMode === "list"}
<div class="tag-row selectable">
{#each agents.filter((a) => a.id !== editing.id) as agent (agent.id)}
<button class="tag pick" class:picked={editing.delegates.includes(agent.id)} onclick={() => toggleTool(editing.delegates, agent.id)}>{agent.id}</button>
<button class="tag pick" class:picked={editing.delegates.includes(agent.id)} title={agent.description} onclick={() => toggleTool(editing.delegates, agent.id)}>{agent.id}</button>
{/each}
</div>
{/if}
@ -319,7 +371,9 @@
.tag { padding: 2px 8px; border: 1px solid var(--line); border-radius: 4px; color: var(--text-soft); background: var(--code-bg); font-size: 11px; font-family: var(--font-mono); }
.tag-row.selectable .tag { cursor: pointer; user-select: none; }
.tag-row.selectable .tag.picked { border-color: var(--accent-border); color: var(--accent); background: var(--accent-soft); }
.card-actions { display: flex; align-items: center; gap: 8px; }
.card-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.card-actions button { white-space: nowrap; flex-shrink: 0; }
.toolbar-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.icon-button.danger:hover { color: var(--danger); border-color: var(--danger-border); }
.modal-scrim { position: fixed; inset: 0; z-index: 40; border: 0; padding: 0; cursor: default; background: rgba(0,0,0,.45); }
.modal { position: fixed; z-index: 41; top: 6vh; left: 50%; transform: translateX(-50%); width: min(920px, 94vw); max-height: 88vh; overflow: auto; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--panel); box-shadow: var(--shadow-16, var(--shadow-8)); }

View File

@ -235,7 +235,7 @@
<div>
<h2 style="margin:0">子代理活动</h2>
<p style="margin:2px 0 0;color:var(--muted);font-size:12px">
查看活动中的子代理与历史运行;子代理定义在「配置 → 子代理定义」中管理。
查看活动中的子代理与历史运行;子代理定义在「配置 → 子代理」中管理。
</p>
</div>
<button class="secondary" onclick={loadTasks}><Icon name="refresh" size={16} />刷新</button>

View File

@ -121,7 +121,7 @@
<div class="settings-grid">
<Tabs.Root value={tab} onValueChange={changeTab} orientation="vertical">
<Tabs.List class="settings-nav" aria-label="设置分类">
<Tabs.Trigger value="appearance">外观</Tabs.Trigger><Tabs.Trigger value="config">config.json</Tabs.Trigger><Tabs.Trigger value="user">USER.md</Tabs.Trigger><Tabs.Trigger value="agents">AGENTS.md</Tabs.Trigger><Tabs.Trigger value="subagents">子代理定义</Tabs.Trigger>
<Tabs.Trigger value="appearance">外观</Tabs.Trigger><Tabs.Trigger value="config">config.json</Tabs.Trigger><Tabs.Trigger value="user">USER.md</Tabs.Trigger><Tabs.Trigger value="agents">AGENTS.md</Tabs.Trigger><Tabs.Trigger value="subagents">子代理</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>