pi-web-ui 0.68.2 → 0.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/agent-service.js +157 -50
- package/dist/server/attachments.js +7 -2
- package/dist/server/client-state.js +27 -0
- package/dist/server/dsh/dsh-agent-service.js +139 -38
- package/dist/server/dsh/dsh-client.js +9 -8
- package/dist/server/dsh/dsh-sessions.js +4 -3
- package/dist/server/edit-soft-tool.js +33 -26
- package/dist/server/files-service.js +12 -7
- package/dist/server/goal-service.js +85 -24
- package/dist/server/i18n.js +157 -0
- package/dist/server/index.js +76 -18
- package/dist/server/locales.js +55 -1
- package/dist/server/managed.js +61 -0
- package/dist/server/marker-service.js +20 -7
- package/dist/server/markers/builtins/notify.js +19 -6
- package/dist/server/markers/builtins/rename.js +41 -8
- package/dist/server/markers/builtins/todo.js +107 -30
- package/dist/server/markers/registry.js +2 -2
- package/dist/server/mcp-bridge.js +3 -1
- package/dist/server/model-admin.js +25 -14
- package/dist/server/plugin-catalog.js +7 -3
- package/dist/server/plugin-updater.js +6 -2
- package/dist/server/plugins.js +40 -17
- package/dist/server/prompt-composer.js +42 -16
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/scm.js +18 -25
- package/dist/server/serialize.js +1 -0
- package/dist/server/settings-service.js +20 -1
- package/dist/server/subagent-templates.js +105 -0
- package/dist/server/subagents.js +155 -54
- package/dist/server/tabs.js +87 -0
- package/dist/server/terminals.js +88 -48
- package/dist/server/update-check.js +7 -2
- package/dist/server/vision-bridge.js +34 -12
- package/package.json +2 -1
- package/web/dist/assets/{TerminalPanel-BQ5NTB9Y.js → TerminalPanel-Cj8zsjx-.js} +1 -1
- package/web/dist/assets/index-DCOcsPFm.js +334 -0
- package/web/dist/assets/{index-C_I-6Zul.css → index-jH2Bb-0X.css} +1 -1
- package/web/dist/assets/{markdown-DOsihKaR.js → markdown-Cpo0pNcR.js} +1 -1
- package/web/dist/assets/{react-DIP6JKYk.js → react-CtudoG1_.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/dist/assets/index-Ck5pa3XK.js +0 -333
package/dist/server/subagents.js
CHANGED
|
@@ -22,9 +22,13 @@
|
|
|
22
22
|
// 本文件只定义:运行态快照类型、host 接口(由 ClientSession 实现,操作的是
|
|
23
23
|
// 它的 conversation 体系)、以及注册给每个会话的 subagent_* 工具。真正创建
|
|
24
24
|
// conversation / 跑 prompt 全部在 agent-service.ts 的 spawnSubagent 里完成。
|
|
25
|
+
//
|
|
26
|
+
// 双语约定(issue #91):工具 definition 描述走 bilingual(en, zh) 内联双语
|
|
27
|
+
// (英文在前);per-call 返回文本按 lang 取 pick(lang, zh, en)。
|
|
25
28
|
// ---------------------------------------------------------------------------
|
|
26
29
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
27
30
|
import { Type } from "typebox";
|
|
31
|
+
import { bilingual, pick } from "./i18n.js";
|
|
28
32
|
/**
|
|
29
33
|
* subagent_wait_all 的最长阻塞时间:必须短暂低于工具看门狗(默认 20 分钟,
|
|
30
34
|
* PI_WEB_TOOL_TIMEOUT_MS 可调),否则看门狗会先中止整个会话而不是让 wait
|
|
@@ -45,12 +49,28 @@ export function subagentTitle(prompt) {
|
|
|
45
49
|
const line = prompt.split("\n")[0]?.trim() ?? "";
|
|
46
50
|
return line.length > 40 ? `${line.slice(0, 40)}…` : line;
|
|
47
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* 返回注入 ownerId 的 host 包装:每次 spawn 时自动把 ownerId(真正的派发会话)
|
|
54
|
+
* 作为子代理的 parentId 传给底层 host。
|
|
55
|
+
*
|
|
56
|
+
* 背景(issue #95):子代理左栏嵌套靠 parentId,而派发方是某个会话的 runtime ——
|
|
57
|
+
* 必须按 runtime 归属记父对话,而不是派发瞬间的 active。后台对话继续产出时用户
|
|
58
|
+
* 可能已切到别的项目,直接读 activeId 会把孩子记到无关会话名下(错组/沉底)。
|
|
59
|
+
* 每个会话创建 runtime 时用本函数包一层,让它的 spawn 天然带自己的会话 id。
|
|
60
|
+
*/
|
|
61
|
+
export function withSubagentOwner(host, ownerId) {
|
|
62
|
+
return {
|
|
63
|
+
...host,
|
|
64
|
+
spawnSubagent: (prompt, type, cwd, templateName, model) => host.spawnSubagent(prompt, type, cwd, templateName, model, ownerId),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
48
67
|
/**
|
|
49
68
|
* 子代理工具集(注册进每个会话的 customTools,供主 agent 驱动子代理)。
|
|
50
69
|
* 用 `subagent_*` 前缀命名,避免与第三方 pi-subagents 的
|
|
51
70
|
* `Agent`/`get_subagent_result`/`steer_subagent` 冲突。
|
|
52
71
|
*/
|
|
53
|
-
export function makeSubagentTools(host) {
|
|
72
|
+
export function makeSubagentTools(host, lang) {
|
|
73
|
+
const getLang = lang ?? host.lang ?? (() => "en");
|
|
54
74
|
const text = (t, details = {}) => ({
|
|
55
75
|
content: [{ type: "text", text: t }],
|
|
56
76
|
details,
|
|
@@ -59,108 +79,165 @@ export function makeSubagentTools(host) {
|
|
|
59
79
|
defineTool({
|
|
60
80
|
name: "subagent_spawn",
|
|
61
81
|
label: "Spawn subagent",
|
|
62
|
-
description: "
|
|
82
|
+
description: bilingual("Spawn an independent background subagent conversation for a self-contained deliverable task " +
|
|
83
|
+
'(research/implement/review, etc.). Subagents appear in the left "Running conversations" list with a ' +
|
|
84
|
+
"subagent badge; the user can open, supplement, or stop them. The main agent may spawn several in parallel: " +
|
|
85
|
+
"use subagent_wait_all to wait for all at once (no polling), subagent_list for live status, " +
|
|
86
|
+
"subagent_get_result for results, subagent_steer to redirect mid-run, subagent_stop to stop. " +
|
|
87
|
+
"Good for: long-running exploration, parallel research, delegating independent subtasks. Optional template " +
|
|
88
|
+
"param: use a subagent template configured in the settings panel " +
|
|
89
|
+
"(role system prompt + skills/extensions whitelist + optional model); optional model param: explicitly set " +
|
|
90
|
+
'the subagent model (provider/id, e.g. "anthropic/claude-opus-4-5"), which overrides the template and panel ' +
|
|
91
|
+
"default; omit both = follow the main conversation's model.", "在后台启动一个独立的子代理对话,用一个明确的指令去完成一项可独立交付的工作(调研/实现/审查等)。" +
|
|
63
92
|
"子代理会出现在左栏「运行的对话」列表(带子代理标识),用户可点开查看、补充、中止。主 agent 可并行派发多个:" +
|
|
64
93
|
"用 subagent_wait_all 一次性等全部完成(不用轮询)、subagent_list 查看运行态、subagent_get_result 取结果、" +
|
|
65
94
|
"subagent_steer 中途改向、subagent_stop 停止。" +
|
|
66
95
|
"适合:长耗时探索、并行调研、独立子任务委派。可选 template 参数:使用设置面板配置的子代理模板" +
|
|
67
96
|
"(角色系统提示词 + 技能/扩展白名单 + 可选模型);可选 model 参数:显式指定子代理模型(provider/id 格式," +
|
|
68
|
-
'如 "anthropic/claude-opus-4-5"),优先级高于模板与设置面板的默认模型;都不传 = 跟随主对话当前模型。',
|
|
97
|
+
'如 "anthropic/claude-opus-4-5"),优先级高于模板与设置面板的默认模型;都不传 = 跟随主对话当前模型。'),
|
|
69
98
|
promptSnippet: "spawn an independent background subagent for a deliverable task (parallel work)",
|
|
70
99
|
parameters: Type.Object({
|
|
71
|
-
prompt: Type.String({
|
|
72
|
-
|
|
100
|
+
prompt: Type.String({
|
|
101
|
+
description: bilingual("Full instructions for the subagent (goal + constraints + expected output).", "交给子代理的完整指令(要达成的目标 + 约束 + 期望产出)。"),
|
|
102
|
+
}),
|
|
103
|
+
type: Type.Optional(Type.String({
|
|
104
|
+
description: bilingual("Subagent type/role name (e.g. explore/implement/review), for display. Default general.", "子代理类型/角色名(如 explore/implement/review),用于展示。默认 general。"),
|
|
105
|
+
})),
|
|
73
106
|
template: Type.Optional(Type.String({
|
|
74
|
-
description: "
|
|
75
|
-
"
|
|
107
|
+
description: bilingual("Optional: subagent template name (a preset configured under Settings → Subagent Templates, see the " +
|
|
108
|
+
"subagent_templates tool). Template = role system prompt + skills/extensions whitelist + optional " +
|
|
109
|
+
"model; omit = run with the main session defaults.", "可选:子代理模板名(设置面板「子代理模板」配置的预设,见 subagent_templates 工具)。" +
|
|
110
|
+
"模板 = 角色系统提示词 + 技能/扩展白名单 + 可选模型;不传 = 不使用模板,按主会话默认配置运行。"),
|
|
76
111
|
})),
|
|
77
112
|
model: Type.Optional(Type.String({
|
|
78
|
-
description: '
|
|
79
|
-
"
|
|
113
|
+
description: bilingual('Optional: subagent model "provider/id" (e.g. "anthropic/claude-opus-4-5") for this run; overrides the ' +
|
|
114
|
+
"template model and the settings-panel default; omit = template model → panel default → follow the " +
|
|
115
|
+
"main conversation model.", '可选:子代理模型 "provider/id"(如 "anthropic/claude-opus-4-5"),显式指定本次子代理的模型,' +
|
|
116
|
+
"优先级高于模板自带模型与设置面板默认模型;不传 = 模板模型 → 设置面板默认模型 → 跟随主对话当前模型。"),
|
|
117
|
+
})),
|
|
118
|
+
cwd: Type.Optional(Type.String({
|
|
119
|
+
description: bilingual("Subagent working directory (relative/absolute). Defaults to the main session's cwd.", "子代理工作目录(相对/绝对)。默认继承主会话工作目录。"),
|
|
80
120
|
})),
|
|
81
|
-
cwd: Type.Optional(Type.String({ description: "子代理工作目录(相对/绝对)。默认继承主会话工作目录。" })),
|
|
82
121
|
}),
|
|
83
122
|
execute: async (_id, p, _signal, _onUpdate, ctx) => {
|
|
84
123
|
if (p.template && !host.isTemplateUsable(p.template)) {
|
|
85
|
-
return text(`子代理模板不可用:${p.template}(不存在或已停用)。用 subagent_templates 查看当前可用模板清单;不传 template
|
|
124
|
+
return text(pick(getLang(), `子代理模板不可用:${p.template}(不存在或已停用)。用 subagent_templates 查看当前可用模板清单;不传 template 则按默认配置运行。`, `Subagent template unavailable: ${p.template} (missing or disabled). Use subagent_templates to list available templates; omit template to run with defaults.`, "subagents.spawn.template.unavailable", { "p.template": p.template }));
|
|
86
125
|
}
|
|
87
126
|
const convId = await host.spawnSubagent(p.prompt, p.type ?? "general", p.cwd ?? ctx.cwd, p.template, p.model);
|
|
88
|
-
|
|
127
|
+
const subagentType = p.type ?? "general";
|
|
128
|
+
const subagentTitleText = subagentTitle(p.prompt);
|
|
129
|
+
return text(pick(getLang(), `子代理已启动(运行列表可见):${convId}\n类型:${subagentType} · 标题:${subagentTitleText}` +
|
|
89
130
|
(p.template ? `\n模板:${p.template}` : "") +
|
|
90
131
|
(p.model ? `\n模型:${p.model}` : "") +
|
|
91
|
-
`\n用 subagent_wait_all 一次等全部完成(不用轮询),subagent_get_result 取单个结果,subagent_list 看运行态,subagent_steer 改向,subagent_stop 停止。`,
|
|
132
|
+
`\n用 subagent_wait_all 一次等全部完成(不用轮询),subagent_get_result 取单个结果,subagent_list 看运行态,subagent_steer 改向,subagent_stop 停止。`, `Subagent started (visible in the running list): ${convId}\nType: ${subagentType} · Title: ${subagentTitleText}` +
|
|
133
|
+
(p.template ? `\nTemplate: ${p.template}` : "") +
|
|
134
|
+
(p.model ? `\nModel: ${p.model}` : "") +
|
|
135
|
+
`\nUse subagent_wait_all to wait for all at once (no polling), subagent_get_result for a single result, subagent_list for live status, subagent_steer to redirect, subagent_stop to stop.`, "subagents.spawn.started", {
|
|
136
|
+
convId: convId,
|
|
137
|
+
subagentType: subagentType,
|
|
138
|
+
subagentTitleText: subagentTitleText,
|
|
139
|
+
"p.template": p.template,
|
|
140
|
+
"p.model": p.model,
|
|
141
|
+
}), { convId, template: p.template, model: p.model });
|
|
92
142
|
},
|
|
93
143
|
}),
|
|
94
144
|
defineTool({
|
|
95
145
|
name: "subagent_get_result",
|
|
96
146
|
label: "Get subagent result",
|
|
97
|
-
description: "
|
|
147
|
+
description: bilingual("Fetch a subagent's result or current progress. If not finished yet, returns the current status and partial " +
|
|
148
|
+
"output; runtime errors (e.g. provider 400) are surfaced here as explicit errors.", "取一个子代理的结果或当前运行态。若尚未完成,返回当前状态与已产出的文本;运行报错(如 provider 400)会在这里明确标出错误。"),
|
|
98
149
|
promptSnippet: "fetch a subagent's result / current progress",
|
|
99
150
|
parameters: Type.Object({
|
|
100
|
-
runId: Type.String({
|
|
151
|
+
runId: Type.String({
|
|
152
|
+
description: bilingual("ConvId returned by subagent_spawn. Clicking the same conversation in the left panel opens it directly.", "subagent_spawn 返回的 convId。左栏点击同名对话可直接查看。"),
|
|
153
|
+
}),
|
|
101
154
|
}),
|
|
102
155
|
execute: async (_id, p) => {
|
|
103
156
|
const r = host.getSubagent(p.runId);
|
|
157
|
+
const missingId = shortId(p.runId);
|
|
104
158
|
if (!r)
|
|
105
|
-
return text(`未找到子代理 ${
|
|
106
|
-
const verdict = subagentVerdict(r);
|
|
159
|
+
return text(pick(getLang(), `未找到子代理 ${missingId}(可能已移出)。`, `Subagent ${missingId} not found (may have been dismissed).`, "subagents.get.not.found", { missingId: missingId }), undefined);
|
|
160
|
+
const verdict = subagentVerdict(r, getLang());
|
|
161
|
+
const doneId = shortId(r.convId);
|
|
162
|
+
const doneDetail = verdictText(r, getLang());
|
|
163
|
+
const doneOutput = r.output || (getLang() === "zh" ? "(无结果)" : "(no result)");
|
|
107
164
|
if (r.streaming || r.state === "running") {
|
|
108
|
-
|
|
165
|
+
const runningId = shortId(r.convId);
|
|
166
|
+
const runningOutput = r.output || (getLang() === "zh" ? "(暂无输出)" : "(no output yet)");
|
|
167
|
+
return text(pick(getLang(), `子代理 ${runningId}(${r.type})仍在运行(状态 ${r.state})。\n当前输出:\n${runningOutput}`, `Subagent ${runningId} (${r.type}) is still running (state ${r.state}).\nCurrent output:\n${runningOutput}`, "subagents.get.running", { runningId: runningId, "r.type": r.type, "r.state": r.state, runningOutput: runningOutput }), r);
|
|
109
168
|
}
|
|
110
|
-
return text(`子代理 ${
|
|
169
|
+
return text(pick(getLang(), `子代理 ${doneId}(${r.type})状态:${verdict}\n${doneDetail}\n${doneOutput}`, `Subagent ${doneId} (${r.type}) status: ${verdict}\n${doneDetail}\n${doneOutput}`, "subagents.get.done", { doneId: doneId, "r.type": r.type, verdict: verdict, doneDetail: doneDetail, doneOutput: doneOutput }), r);
|
|
111
170
|
},
|
|
112
171
|
}),
|
|
113
172
|
defineTool({
|
|
114
173
|
name: "subagent_steer",
|
|
115
174
|
label: "Steer subagent",
|
|
116
|
-
description: "向一个子代理注入一条消息,重定向/补充它的工作方向(等同用户在它的对话里发消息)。",
|
|
175
|
+
description: bilingual("Inject a message into a subagent to redirect or supplement its work (same as the user sending a message in its conversation).", "向一个子代理注入一条消息,重定向/补充它的工作方向(等同用户在它的对话里发消息)。"),
|
|
117
176
|
promptSnippet: "inject a message into a running subagent to redirect its work",
|
|
118
177
|
parameters: Type.Object({
|
|
119
|
-
runId: Type.String({
|
|
120
|
-
|
|
178
|
+
runId: Type.String({
|
|
179
|
+
description: bilingual("Target subagent convId.", "目标子代理 convId。"),
|
|
180
|
+
}),
|
|
181
|
+
message: Type.String({
|
|
182
|
+
description: bilingual("Redirect / supplementary info to inject.", "要注入的方向调整/补充信息。"),
|
|
183
|
+
}),
|
|
121
184
|
}),
|
|
122
185
|
execute: async (_id, p) => {
|
|
123
186
|
await host.steerSubagent(p.runId, p.message);
|
|
124
|
-
|
|
187
|
+
const steerId = shortId(p.runId);
|
|
188
|
+
return text(pick(getLang(), `已向子代理 ${steerId} 注入消息。`, `Message injected into subagent ${steerId}.`, "subagents.steer.injected", { steerId: steerId }));
|
|
125
189
|
},
|
|
126
190
|
}),
|
|
127
191
|
defineTool({
|
|
128
192
|
name: "subagent_list",
|
|
129
193
|
label: "List subagents",
|
|
130
|
-
description: "列出全部子代理的运行态:convId、类型、状态、标题、消息数(报错/中止的会在状态里标出)。",
|
|
194
|
+
description: bilingual("List all subagents and their live status: convId, type, state, title, message count (errors/aborts are marked in the state).", "列出全部子代理的运行态:convId、类型、状态、标题、消息数(报错/中止的会在状态里标出)。"),
|
|
131
195
|
promptSnippet: "list all subagents and their live status",
|
|
132
196
|
parameters: Type.Object({}),
|
|
133
197
|
execute: async () => {
|
|
134
198
|
const list = host.listSubagents();
|
|
135
199
|
if (list.length === 0)
|
|
136
|
-
return text("当前没有子代理。");
|
|
137
|
-
const
|
|
200
|
+
return text(pick(getLang(), "当前没有子代理。", "No subagents running.", "subagents.list.empty"));
|
|
201
|
+
const tLang = getLang();
|
|
202
|
+
const lines = list.map((r) => `- ${r.convId} · ${r.type} · ${subagentVerdict(r, tLang)} · ${r.title}` +
|
|
203
|
+
(tLang === "zh" ? `(msg: ${r.messageCount})` : ` (msg: ${r.messageCount})`));
|
|
138
204
|
return text(lines.join("\n"));
|
|
139
205
|
},
|
|
140
206
|
}),
|
|
141
207
|
defineTool({
|
|
142
208
|
name: "subagent_stop",
|
|
143
209
|
label: "Stop subagent",
|
|
144
|
-
description: "停止一个运行中的子代理(等同用户在它的对话里点中止)。已完成的不受影响。",
|
|
210
|
+
description: bilingual("Stop a running subagent (same as the user aborting it in its conversation). Already-finished ones are unaffected.", "停止一个运行中的子代理(等同用户在它的对话里点中止)。已完成的不受影响。"),
|
|
145
211
|
promptSnippet: "stop a running subagent",
|
|
146
|
-
parameters: Type.Object({
|
|
212
|
+
parameters: Type.Object({
|
|
213
|
+
runId: Type.String({
|
|
214
|
+
description: bilingual("Target subagent convId.", "目标子代理 convId。"),
|
|
215
|
+
}),
|
|
216
|
+
}),
|
|
147
217
|
execute: async (_id, p) => {
|
|
148
218
|
await host.stopSubagent(p.runId);
|
|
149
|
-
|
|
219
|
+
const stopId = shortId(p.runId);
|
|
220
|
+
return text(pick(getLang(), `已请求停止子代理 ${stopId}。`, `Stop requested for subagent ${stopId}.`, "subagents.stop.requested", { stopId: stopId }));
|
|
150
221
|
},
|
|
151
222
|
}),
|
|
152
223
|
defineTool({
|
|
153
224
|
name: "subagent_wait_all",
|
|
154
225
|
label: "Wait for subagents",
|
|
155
|
-
description: "
|
|
226
|
+
description: bilingual("Wait for multiple subagents to finish at once (blocks this round until all reach a terminal state or time out), " +
|
|
227
|
+
"then summarize each result/error — no need to poll subagent_get_result. Pass runIds for specific subagents " +
|
|
228
|
+
"(convIds returned by subagent_spawn); omit = wait for all currently running ones. On timeout or abort of this " +
|
|
229
|
+
"round, returns the remaining unfinished list; call again to continue waiting. " +
|
|
230
|
+
"Good for: collecting parallel subagents.", "一次性等待多个子代理全部完成(阻塞本回合直到它们都到达终态或超时),然后汇总返回每个的结果/错误——" +
|
|
156
231
|
"不用反复调 subagent_get_result 轮询。传 runIds 指定要等的子代理(subagent_spawn 返回的 convId);" +
|
|
157
232
|
"不传 = 等当前全部运行中的子代理。超时或本轮被中止时返回剩余未完成名单,可再次调用继续等。" +
|
|
158
|
-
"适合:并行派发多个子代理后收口。",
|
|
233
|
+
"适合:并行派发多个子代理后收口。"),
|
|
159
234
|
promptSnippet: "wait for multiple subagents to finish (no polling) and get all results",
|
|
160
235
|
parameters: Type.Object({
|
|
161
|
-
runIds: Type.Optional(Type.Array(Type.String({
|
|
236
|
+
runIds: Type.Optional(Type.Array(Type.String({
|
|
237
|
+
description: bilingual("ConvId of a subagent to wait for (returned by subagent_spawn). Omit = wait for all currently running.", "要等待的子代理 convId(subagent_spawn 返回值)。缺省 = 等当前全部运行中的。"),
|
|
238
|
+
}))),
|
|
162
239
|
timeoutSeconds: Type.Optional(Type.Integer({
|
|
163
|
-
description: `最多等待秒数(默认 600,上限约 ${Math.floor(WAIT_CAP_MS / 1000)}
|
|
240
|
+
description: bilingual(`Max wait in seconds (default 600, cap ~${Math.floor(WAIT_CAP_MS / 1000)} — must stay below the tool watchdog; on timeout returns the unfinished list so you can call again).`, `最多等待秒数(默认 600,上限约 ${Math.floor(WAIT_CAP_MS / 1000)}——必须低于工具看门狗,超时返回未完成名单可再调)。`),
|
|
164
241
|
minimum: 1,
|
|
165
242
|
maximum: Math.floor(WAIT_CAP_MS / 1000),
|
|
166
243
|
})),
|
|
@@ -179,41 +256,62 @@ export function makeSubagentTools(host) {
|
|
|
179
256
|
while (pending().length > 0 && Date.now() < deadline && !(signal?.aborted ?? false)) {
|
|
180
257
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
181
258
|
}
|
|
259
|
+
const tLang = getLang();
|
|
182
260
|
const remaining = pending();
|
|
183
261
|
const lines = [...wanted]
|
|
184
262
|
.map((id) => {
|
|
185
263
|
const r = host.getSubagent(id);
|
|
186
264
|
if (!r)
|
|
187
|
-
return
|
|
188
|
-
|
|
189
|
-
|
|
265
|
+
return tLang === "zh"
|
|
266
|
+
? `- ${shortId(id)}:未找到(可能已移出)`
|
|
267
|
+
: `- ${shortId(id)}: not found (may have been dismissed)`;
|
|
268
|
+
const body = verdictText(r, tLang);
|
|
269
|
+
return ((tLang === "zh"
|
|
270
|
+
? `- ${shortId(r.convId)}(${r.type})· ${r.title} · ${subagentVerdict(r, tLang)}`
|
|
271
|
+
: `- ${shortId(r.convId)} (${r.type}) · ${r.title} · ${subagentVerdict(r, tLang)}`) +
|
|
190
272
|
(body ? `\n ${body}` : "") +
|
|
191
273
|
(r.output ? `\n ${r.output.split("\n").slice(0, 30).join("\n ")}` : ""));
|
|
192
274
|
})
|
|
193
275
|
.join("\n");
|
|
276
|
+
const timeoutSecs = Math.round(timeoutMs / 1000);
|
|
277
|
+
const waitedSecs = Math.round((Date.now() - waitStart) / 1000);
|
|
194
278
|
const head = remaining.length === 0
|
|
195
|
-
? `全部 ${wanted.size}
|
|
279
|
+
? pick(tLang, `全部 ${wanted.size} 个子代理已收口:`, `All ${wanted.size} subagent(s) collected:`, "subagents.wait.collected", { "wanted.size": wanted.size })
|
|
196
280
|
: signal?.aborted
|
|
197
|
-
? `本轮被中止,${remaining.length}
|
|
198
|
-
: `等待超过 ${
|
|
199
|
-
return text(`${head}\n${lines}\n` + promptRemaining(remaining));
|
|
281
|
+
? pick(tLang, `本轮被中止,${remaining.length} 个仍在运行:`, `This round was aborted, ${remaining.length} still running:`, "subagents.wait.aborted", { "remaining.length": remaining.length })
|
|
282
|
+
: pick(tLang, `等待超过 ${timeoutSecs}s 超时(实际等待 ${waitedSecs}s),${remaining.length} 个仍在运行:`, `Wait timed out after ${timeoutSecs}s (actually waited ${waitedSecs}s), ${remaining.length} still running:`, "subagents.wait.timeout", { timeoutSecs: timeoutSecs, waitedSecs: waitedSecs, "remaining.length": remaining.length });
|
|
283
|
+
return text(`${head}\n${lines}\n` + promptRemaining(remaining, tLang));
|
|
200
284
|
},
|
|
201
285
|
}),
|
|
202
286
|
defineTool({
|
|
203
287
|
name: "subagent_templates",
|
|
204
288
|
label: "List subagent templates",
|
|
205
|
-
description: "
|
|
206
|
-
"
|
|
289
|
+
description: bilingual("List the configurable subagent templates (role system prompt + skills/extensions whitelist + optional model " +
|
|
290
|
+
"presets) for the subagent_spawn template param. Disabled templates never appear here. " +
|
|
291
|
+
"Empty list = no templates configured; subagents run with defaults.", "列出设置面板「子代理模板」配置的可用模板(角色系统提示词 + 技能/扩展白名单 + 可选模型 的组合预设)," +
|
|
292
|
+
"供 subagent_spawn 的 template 参数选用。已停用的模板不会出现在这里。list 为空 = 未配置模板,子代理按默认配置运行。"),
|
|
207
293
|
promptSnippet: "list configurable subagent templates (role prompt + skills/extensions whitelist presets)",
|
|
208
294
|
parameters: Type.Object({}),
|
|
209
295
|
execute: async () => {
|
|
210
296
|
const list = host.listTemplates();
|
|
297
|
+
const tLang = getLang();
|
|
211
298
|
if (list.length === 0) {
|
|
212
|
-
return text("当前没有可用的子代理模板(设置面板 → 子代理模板 添加后可用)。子代理默认按主会话配置运行。");
|
|
299
|
+
return text(pick(tLang, "当前没有可用的子代理模板(设置面板 → 子代理模板 添加后可用)。子代理默认按主会话配置运行。", "No subagent templates available (add some under Settings → Subagent Templates). Subagents run with the main session defaults.", "subagents.templates.empty"));
|
|
213
300
|
}
|
|
214
|
-
const lines = list.map((t) =>
|
|
215
|
-
|
|
216
|
-
|
|
301
|
+
const lines = list.map((t) => {
|
|
302
|
+
const desc = tLang === "zh" ? t.description : t.descriptionEn || t.description;
|
|
303
|
+
return ((tLang === "zh" ? `- ${t.name}${desc ? `:${desc}` : ""}` : `- ${t.name}${desc ? `: ${desc}` : ""}`) +
|
|
304
|
+
(tLang === "zh"
|
|
305
|
+
? t.model
|
|
306
|
+
? `(模型:${t.model})`
|
|
307
|
+
: "(跟随主对话模型)"
|
|
308
|
+
: t.model
|
|
309
|
+
? ` (model: ${t.model})`
|
|
310
|
+
: " (follows the main conversation model)"));
|
|
311
|
+
});
|
|
312
|
+
const firstTemplateName = list[0]?.name;
|
|
313
|
+
const templateLines = lines.join("\n");
|
|
314
|
+
return text(pick(getLang(), `可用的子代理模板(subagent_spawn 的 template 参数传名字,如 subagent_spawn(template="${firstTemplateName}")):\n${templateLines}`, `Available subagent templates (pass the name as subagent_spawn's template param, e.g. subagent_spawn(template="${firstTemplateName}")):\n${templateLines}`, "subagents.templates.list", { firstTemplateName: firstTemplateName, templateLines: templateLines }));
|
|
217
315
|
},
|
|
218
316
|
}),
|
|
219
317
|
];
|
|
@@ -223,24 +321,27 @@ function shortId(id) {
|
|
|
223
321
|
return id.slice(0, 8);
|
|
224
322
|
}
|
|
225
323
|
/** 人类可读的终态判定:报错 > 中止 > done > running。 */
|
|
226
|
-
function subagentVerdict(r) {
|
|
324
|
+
function subagentVerdict(r, lang = "en") {
|
|
227
325
|
if (r.error)
|
|
228
|
-
return "error(报错)";
|
|
326
|
+
return pick(lang, "error(报错)", "error", "subagents.verdict.error");
|
|
229
327
|
if (r.canceled)
|
|
230
|
-
return "canceled(已中止)";
|
|
328
|
+
return pick(lang, "canceled(已中止)", "canceled", "subagents.verdict.canceled");
|
|
231
329
|
return r.state;
|
|
232
330
|
}
|
|
233
331
|
/** 终态的可读说明(错误文本 / 中止说明 / 空)。运行中返回空。 */
|
|
234
|
-
function verdictText(r) {
|
|
332
|
+
function verdictText(r, lang = "en") {
|
|
235
333
|
if (r.error)
|
|
236
|
-
return `错误:${r.error}
|
|
334
|
+
return pick(lang, `错误:${r.error}`, `Error: ${r.error}`, "subagents.verdict.error.detail", {
|
|
335
|
+
"r.error": r.error,
|
|
336
|
+
});
|
|
237
337
|
if (r.canceled)
|
|
238
|
-
return "(被中止,未产出结论)";
|
|
338
|
+
return pick(lang, "(被中止,未产出结论)", "(Aborted, no conclusion produced.)", "subagents.verdict.aborted");
|
|
239
339
|
return "";
|
|
240
340
|
}
|
|
241
341
|
/** 未完成部分的引导文案。 */
|
|
242
|
-
function promptRemaining(remaining) {
|
|
342
|
+
function promptRemaining(remaining, lang = "en") {
|
|
243
343
|
if (remaining.length === 0)
|
|
244
344
|
return "";
|
|
245
|
-
|
|
345
|
+
const pendingIds = remaining.map(shortId).join(", ");
|
|
346
|
+
return pick(lang, `\n未完成:${pendingIds}。可再次调用 subagent_wait_all(或 subagent_steer 补充指令 / subagent_stop 中止)。`, `\nPending: ${pendingIds}. You may call subagent_wait_all again (or subagent_steer to add instructions / subagent_stop to abort).`, "subagents.wait.pending", { pendingIds: pendingIds });
|
|
246
347
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tabs — choose what an instance offers.
|
|
3
|
+
*
|
|
4
|
+
* pi-web-ui shows Chat, Terminal, Git, Search, Background tasks and Settings,
|
|
5
|
+
* always, plus a tab per installed plugin. On the machine you are working on
|
|
6
|
+
* that is the point of the tool. Exposed to other people it is not: Terminal
|
|
7
|
+
* opens a shell as the server's user, and Git shows the working copy with
|
|
8
|
+
* commit and diff a click away. Today there is no way to leave those out.
|
|
9
|
+
*
|
|
10
|
+
* `PI_WEB_TABS=chat,search,settings` is that way. Absent — the default —
|
|
11
|
+
* means every tab, so nothing changes for anybody who does not set it.
|
|
12
|
+
*
|
|
13
|
+
* The rule that shapes the whole thing: **a hidden tab whose messages the
|
|
14
|
+
* server still accepts is a hidden tab, not a disabled one.** Anything that
|
|
15
|
+
* can open the WebSocket can send `terminal_create`. So the client stops
|
|
16
|
+
* drawing them and the server stops answering them, and the second half is the
|
|
17
|
+
* one that matters.
|
|
18
|
+
*
|
|
19
|
+
* Chat is never off: it is the application.
|
|
20
|
+
*/
|
|
21
|
+
/** Every tab that can be listed. `chat` is always on and is listed for symmetry. */
|
|
22
|
+
export const ALL_TABS = ["chat", "terminal", "git", "search", "tasks", "settings", "plugins"];
|
|
23
|
+
/**
|
|
24
|
+
* The messages each tab owns exclusively.
|
|
25
|
+
*
|
|
26
|
+
* Only exclusive ones: `abort_bash` stops the agent's own bash tool inside a
|
|
27
|
+
* chat answer, not a terminal the user opened, so it is not listed — turning
|
|
28
|
+
* off the Terminal tab must not change what the agent can do in a conversation.
|
|
29
|
+
*
|
|
30
|
+
* `run_command` is listed under terminal because it starts a process in one:
|
|
31
|
+
* leaving it out would take the tab away and leave the shell reachable, which
|
|
32
|
+
* is exactly the failure this module exists to prevent.
|
|
33
|
+
*/
|
|
34
|
+
const OWNED = {
|
|
35
|
+
terminal: ["terminal_create", "terminal_input", "terminal_resize", "terminal_kill", "rename_terminal", "run_command"],
|
|
36
|
+
git: ["scm_status", "scm_history", "scm_filediff", "scm_commit"],
|
|
37
|
+
tasks: ["list_bg_servers", "kill_background_server", "kill_background_servers"],
|
|
38
|
+
search: ["search_files", "search_sessions"],
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The allow-list, or null when there is none.
|
|
42
|
+
*
|
|
43
|
+
* Case and spaces are forgiven because this is written by hand in a systemd
|
|
44
|
+
* unit or a compose file. Unknown names are kept rather than rejected: a
|
|
45
|
+
* plugin tab, or a tab a later version adds, should not make the server fail
|
|
46
|
+
* to start.
|
|
47
|
+
*/
|
|
48
|
+
export function parseTabs(env = process.env) {
|
|
49
|
+
const raw = (env.PI_WEB_TABS ?? "").trim();
|
|
50
|
+
if (!raw)
|
|
51
|
+
return null;
|
|
52
|
+
const tabs = new Set(raw
|
|
53
|
+
.split(",")
|
|
54
|
+
.map((t) => t.trim().toLowerCase())
|
|
55
|
+
.filter(Boolean));
|
|
56
|
+
if (tabs.size === 0)
|
|
57
|
+
return null;
|
|
58
|
+
tabs.add("chat");
|
|
59
|
+
return tabs;
|
|
60
|
+
}
|
|
61
|
+
/** Whether a tab is offered. No list means every tab. */
|
|
62
|
+
export function isTabAllowed(tab, tabs) {
|
|
63
|
+
if (!tabs)
|
|
64
|
+
return true;
|
|
65
|
+
if (tab === "chat")
|
|
66
|
+
return true;
|
|
67
|
+
return tabs.has(tab.toLowerCase());
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The reason to refuse a message, or null when it may proceed.
|
|
71
|
+
*
|
|
72
|
+
* Returning the sentence rather than a boolean keeps the call site one line and
|
|
73
|
+
* puts the explanation next to the rule: the user sees why, instead of a
|
|
74
|
+
* message that vanishes.
|
|
75
|
+
*/
|
|
76
|
+
export function tabsRefusal(type, tabs) {
|
|
77
|
+
if (!tabs)
|
|
78
|
+
return null;
|
|
79
|
+
for (const [tab, messages] of Object.entries(OWNED)) {
|
|
80
|
+
if (!messages.includes(type))
|
|
81
|
+
continue;
|
|
82
|
+
if (isTabAllowed(tab, tabs))
|
|
83
|
+
return null;
|
|
84
|
+
return `The ${tab} tab is not enabled on this instance (PI_WEB_TABS).`;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|