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:
parent
b13450498b
commit
7de7a40de7
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "picobot"
|
name = "picobot"
|
||||||
version = "1.13.0"
|
version = "1.13.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
4
webui/package-lock.json
generated
4
webui/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picobot-webui",
|
"name": "picobot-webui",
|
||||||
"version": "1.13.0",
|
"version": "1.13.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picobot-webui",
|
"name": "picobot-webui",
|
||||||
"version": "1.13.0",
|
"version": "1.13.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bits-ui": "^2.0.0",
|
"bits-ui": "^2.0.0",
|
||||||
"dompurify": "^3.4.12",
|
"dompurify": "^3.4.12",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "picobot-webui",
|
"name": "picobot-webui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.13.0",
|
"version": "1.13.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
|
|||||||
@ -10,8 +10,23 @@
|
|||||||
let error = $state("");
|
let error = $state("");
|
||||||
let editing = $state(null);
|
let editing = $state(null);
|
||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
|
let reloading = $state(false);
|
||||||
|
let reloadStatus = $state(null);
|
||||||
|
let pollTimer = null;
|
||||||
let { notify } = $props();
|
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 = () => ({
|
const blank = () => ({
|
||||||
id: "",
|
id: "",
|
||||||
description: "",
|
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) {
|
function toggleTool(list, name) {
|
||||||
const i = list.indexOf(name);
|
const i = list.indexOf(name);
|
||||||
if (i >= 0) list.splice(i, 1);
|
if (i >= 0) list.splice(i, 1);
|
||||||
@ -132,7 +174,7 @@
|
|||||||
else if (editing.delegateMode === "list") payload.delegates = editing.delegates;
|
else if (editing.delegateMode === "list") payload.delegates = editing.delegates;
|
||||||
await api("/api/agents", { method: "POST", body: JSON.stringify(payload) });
|
await api("/api/agents", { method: "POST", body: JSON.stringify(payload) });
|
||||||
editing = null;
|
editing = null;
|
||||||
notify("子代理已保存(需重载配置生效)");
|
notify("子代理已保存,点击「热重载」使其生效");
|
||||||
await load();
|
await load();
|
||||||
} catch (caught) {
|
} catch (caught) {
|
||||||
notify(caught.message, true);
|
notify(caught.message, true);
|
||||||
@ -181,7 +223,11 @@
|
|||||||
return tool?.description || "";
|
return tool?.description || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(load);
|
onMount(() => {
|
||||||
|
load();
|
||||||
|
startPolling();
|
||||||
|
return stopPolling;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="definitions">
|
<div class="definitions">
|
||||||
@ -189,11 +235,17 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 style="margin:0">具名子代理</h2>
|
<h2 style="margin:0">具名子代理</h2>
|
||||||
<p style="margin:2px 0 0;color:var(--muted);font-size:12px">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<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>
|
<button class="primary" onclick={startNew}><Icon name="add" size={16} />新增子代理</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="loading">加载中…</div>
|
<div class="loading">加载中…</div>
|
||||||
@ -239,7 +291,7 @@
|
|||||||
<button type="button" class="modal-scrim" onclick={cancelEdit} aria-label="关闭" tabindex="-1"></button>
|
<button type="button" class="modal-scrim" onclick={cancelEdit} aria-label="关闭" tabindex="-1"></button>
|
||||||
<div class="modal" role="dialog" aria-label="编辑子代理">
|
<div class="modal" role="dialog" aria-label="编辑子代理">
|
||||||
<div class="editor-head">
|
<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>
|
<button class="icon-button" aria-label="关闭" onclick={cancelEdit}><Icon name="dismiss" size={18} /></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="agent-form">
|
<div class="agent-form">
|
||||||
@ -288,7 +340,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-label">可继续委托给 <small>(该子代理可再委托给谁)</small></div>
|
<div class="form-label">递归委托 <small>(该子代理可再委托给哪些子代理)</small></div>
|
||||||
<select bind:value={editing.delegateMode}>
|
<select bind:value={editing.delegateMode}>
|
||||||
<option value="default">默认:仅 general-purpose</option>
|
<option value="default">默认:仅 general-purpose</option>
|
||||||
<option value="none">不可继续委托</option>
|
<option value="none">不可继续委托</option>
|
||||||
@ -298,7 +350,7 @@
|
|||||||
{#if editing.delegateMode === "list"}
|
{#if editing.delegateMode === "list"}
|
||||||
<div class="tag-row selectable">
|
<div class="tag-row selectable">
|
||||||
{#each agents.filter((a) => a.id !== editing.id) as agent (agent.id)}
|
{#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}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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 { 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 { cursor: pointer; user-select: none; }
|
||||||
.tag-row.selectable .tag.picked { border-color: var(--accent-border); color: var(--accent); background: var(--accent-soft); }
|
.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); }
|
.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-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)); }
|
.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)); }
|
||||||
|
|||||||
@ -235,7 +235,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 style="margin:0">子代理活动</h2>
|
<h2 style="margin:0">子代理活动</h2>
|
||||||
<p style="margin:2px 0 0;color:var(--muted);font-size:12px">
|
<p style="margin:2px 0 0;color:var(--muted);font-size:12px">
|
||||||
查看活动中的子代理与历史运行;子代理定义在「配置 → 子代理定义」中管理。
|
查看活动中的子代理与历史运行;子代理定义在「配置 → 子代理」中管理。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="secondary" onclick={loadTasks}><Icon name="refresh" size={16} />刷新</button>
|
<button class="secondary" onclick={loadTasks}><Icon name="refresh" size={16} />刷新</button>
|
||||||
|
|||||||
@ -121,7 +121,7 @@
|
|||||||
<div class="settings-grid">
|
<div class="settings-grid">
|
||||||
<Tabs.Root value={tab} onValueChange={changeTab} orientation="vertical">
|
<Tabs.Root value={tab} onValueChange={changeTab} orientation="vertical">
|
||||||
<Tabs.List class="settings-nav" aria-label="设置分类">
|
<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.List>
|
||||||
</Tabs.Root>
|
</Tabs.Root>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user