pi-web-ui 0.76.0 → 0.78.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.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 排队消息(插队 steer / 排队 followUp)纯函数 —— 独立成模块是为了可单测
3
+ * (`AgentService` 依赖 SDK 与运行时,不适合在单测里 import)。
4
+ *
5
+ * 背景:pi SDK 没有「按条操作队列」的 API,服务端移除一条排队消息的做法是
6
+ * `clearQueue()` 之后把幸存者按原顺序重新入队(见 `AgentService.removeQueued`)。
7
+ * 重建时若用值过滤(`list.filter((t) => t !== text)`),会把**所有**同文本项一起删掉;
8
+ * 而气泡上的 ✕ 只对应一条消息、本地显示镜像也只移除一条 → 同一条文本被排队两次时
9
+ * 「点一次 ✕ 删两条」,且显示与真实队列在下次 `queue_update` 之前不一致。
10
+ * 因此统一按「只移除第一处匹配」处理。
11
+ */
12
+ /** 移除列表中第一处等于 `text` 的项;找不到时返回入参的拷贝(不修改入参)。 */
13
+ export function removeFirstOccurrence(list, text) {
14
+ const index = list.indexOf(text);
15
+ if (index < 0)
16
+ return [...list];
17
+ return [...list.slice(0, index), ...list.slice(index + 1)];
18
+ }
@@ -8,9 +8,10 @@
8
8
  */
9
9
  import { existsSync, readdirSync } from "node:fs";
10
10
  import { basename, dirname, join } from "node:path";
11
- import { extensionKey, normalizeRetryMaxAttempts, } from "./client-state.js";
11
+ import { extensionKey, normalizeRetryMaxAttempts, normalizeSkillList, } from "./client-state.js";
12
12
  import { findVisionModels, SYSTEM_PROMPT } from "./vision-bridge.js";
13
13
  import { DEFAULT_TEMPLATES } from "./subagent-templates.js";
14
+ import { deriveLegacy, foldLegacyIntoDisabled, normalizeDisabledAgentTools } from "./tool-manager.js";
14
15
  export class SettingsService {
15
16
  host;
16
17
  templates;
@@ -141,13 +142,21 @@ export class SettingsService {
141
142
  // 推送都会把已删除的 skill 以灰条形式永恒地补回面板(“关闭过的
142
143
  // skill 被一直记录”)。session 未就绪时保守跳过。
143
144
  if (loadedSkillNames !== null) {
144
- const stale = [...new Set([...this.settings.disabledSkills, ...this.settings.reviewDisabledSkills])].filter((name) => !loadedSkillNames.has(name) && !this.skillStillOnDisk(name));
145
+ const stale = [
146
+ ...new Set([
147
+ ...this.settings.disabledSkills,
148
+ ...this.settings.reviewDisabledSkills,
149
+ ...normalizeSkillList(this.settings.skillsFullText),
150
+ ]),
151
+ ].filter((name) => !loadedSkillNames.has(name) && !this.skillStillOnDisk(name));
145
152
  if (stale.length > 0) {
146
153
  this.settings.disabledSkills = this.settings.disabledSkills.filter((n) => !stale.includes(n));
147
154
  this.settings.reviewDisabledSkills = this.settings.reviewDisabledSkills.filter((n) => !stale.includes(n));
155
+ this.settings.skillsFullText = normalizeSkillList(this.settings.skillsFullText).filter((n) => !stale.includes(n));
148
156
  this.host.stateStore.saveSettings(this.host.clientId, {
149
157
  disabledSkills: this.settings.disabledSkills,
150
158
  reviewDisabledSkills: this.settings.reviewDisabledSkills,
159
+ skillsFullText: this.settings.skillsFullText,
151
160
  });
152
161
  }
153
162
  }
@@ -180,6 +189,8 @@ export class SettingsService {
180
189
  const extensions = [...this.knownExtensions.values()]
181
190
  .map((e) => ({ ...e, enabled: !disabledExts.has(e.id) }))
182
191
  .sort((a, b) => a.name.localeCompare(b.name));
192
+ // 统一工具开关是单源(disabledAgentTools),遗留三开关推送时推导,保证面板一致。
193
+ const legacyTools = deriveLegacy(this.settings.disabledAgentTools ?? []);
183
194
  // 当前会话提示词快照:完整生效文本 + 各来源默认(自动)内容(只读预览)。
184
195
  const promptSnap = this.host.promptSnapshot();
185
196
  this.host.emit({
@@ -189,11 +200,12 @@ export class SettingsService {
189
200
  customSystemPrompt: this.settings.customSystemPrompt,
190
201
  promptTemplate: this.settings.promptTemplate ?? "",
191
202
  promptOverrides: { ...this.settings.promptOverrides },
192
- terminalToolsEnabled: this.settings.terminalToolsEnabled,
203
+ disabledAgentTools: [...normalizeDisabledAgentTools(this.settings.disabledAgentTools)],
204
+ terminalToolsEnabled: legacyTools.terminalToolsEnabled,
193
205
  terminalBash: this.settings.terminalBash,
194
206
  terminalBashIdleMs: this.settings.terminalBashIdleMs,
195
- editSoftEnabled: this.settings.editSoftEnabled,
196
- questionnaireEnabled: this.settings.questionnaireEnabled,
207
+ editSoftEnabled: legacyTools.editSoftEnabled,
208
+ questionnaireEnabled: legacyTools.questionnaireEnabled,
197
209
  goalModeEnabled: this.settings.goalModeEnabled,
198
210
  thinkingWrap: this.settings.thinkingWrap,
199
211
  toolsWrap: this.settings.toolsWrap,
@@ -204,6 +216,7 @@ export class SettingsService {
204
216
  reviewPrompt: this.settings.reviewPrompt,
205
217
  reviewDisabledSkills: [...this.settings.reviewDisabledSkills],
206
218
  disabledPlugins: [...(this.settings.disabledPlugins ?? [])],
219
+ skillsFullText: [...normalizeSkillList(this.settings.skillsFullText)],
207
220
  // The composed system prompt actually in effect (read-only view).
208
221
  effectiveSystemPrompt: promptSnap.full,
209
222
  // 每个来源未覆盖时的默认(自动)内容(「各来源」行预览用)。
@@ -282,9 +295,12 @@ export class SettingsService {
282
295
  partial.promptTemplate !== undefined ||
283
296
  partial.promptOverrides !== undefined ||
284
297
  partial.disabledSkills !== undefined ||
285
- partial.disabledExtensions !== undefined ||
298
+ partial.disabledExtensions !== undefined;
299
+ // 统一工具开关 live 生效(ActiveSet 加减,无需 reload),见末尾 applyToolGating。
300
+ const toolGatingChanged = partial.disabledAgentTools !== undefined ||
286
301
  partial.terminalToolsEnabled !== undefined ||
287
- partial.editSoftEnabled !== undefined;
302
+ partial.editSoftEnabled !== undefined ||
303
+ partial.questionnaireEnabled !== undefined;
288
304
  if (partial.promptMode !== undefined)
289
305
  this.settings.promptMode = partial.promptMode;
290
306
  if (partial.customSystemPrompt !== undefined) {
@@ -314,8 +330,25 @@ export class SettingsService {
314
330
  if (partial.disabledPlugins !== undefined) {
315
331
  this.settings.disabledPlugins = partial.disabledPlugins;
316
332
  }
317
- if (partial.terminalToolsEnabled !== undefined) {
318
- this.settings.terminalToolsEnabled = partial.terminalToolsEnabled;
333
+ // 统一工具开关:新字段优先;只给遗留单开关时折回新字段。两边写完再由
334
+ // deriveLegacy 回填遗留别名,保证内存/推送/落盘三处一致。
335
+ if (partial.disabledAgentTools !== undefined) {
336
+ this.settings.disabledAgentTools = normalizeDisabledAgentTools(partial.disabledAgentTools);
337
+ }
338
+ if (partial.terminalToolsEnabled !== undefined ||
339
+ partial.editSoftEnabled !== undefined ||
340
+ partial.questionnaireEnabled !== undefined) {
341
+ this.settings.disabledAgentTools = foldLegacyIntoDisabled(this.settings.disabledAgentTools ?? [], {
342
+ terminalToolsEnabled: partial.terminalToolsEnabled,
343
+ editSoftEnabled: partial.editSoftEnabled,
344
+ questionnaireEnabled: partial.questionnaireEnabled,
345
+ });
346
+ }
347
+ {
348
+ const legacy = deriveLegacy(this.settings.disabledAgentTools ?? []);
349
+ this.settings.terminalToolsEnabled = legacy.terminalToolsEnabled;
350
+ this.settings.editSoftEnabled = legacy.editSoftEnabled;
351
+ this.settings.questionnaireEnabled = legacy.questionnaireEnabled;
319
352
  }
320
353
  if (partial.terminalBash !== undefined) {
321
354
  this.settings.terminalBash = partial.terminalBash;
@@ -323,13 +356,6 @@ export class SettingsService {
323
356
  if (partial.terminalBashIdleMs !== undefined) {
324
357
  this.settings.terminalBashIdleMs = Math.max(0, Math.floor(partial.terminalBashIdleMs) || 0);
325
358
  }
326
- if (partial.editSoftEnabled !== undefined) {
327
- this.settings.editSoftEnabled = partial.editSoftEnabled;
328
- }
329
- // 问卷开关:运行时无需重载(bridge 处实时读取)。
330
- if (partial.questionnaireEnabled !== undefined) {
331
- this.settings.questionnaireEnabled = partial.questionnaireEnabled;
332
- }
333
359
  // 目标模式总开关:运行时无需重载(goal bar / 服务端入口实时读取)。
334
360
  if (partial.goalModeEnabled !== undefined) {
335
361
  this.settings.goalModeEnabled = partial.goalModeEnabled;
@@ -340,6 +366,11 @@ export class SettingsService {
340
366
  if (partial.toolsWrap !== undefined) {
341
367
  this.settings.toolsWrap = partial.toolsWrap;
342
368
  }
369
+ // 编排模式 / skill 全文注入:before_agent_start 逐 run 实时读取(agent-service
370
+ // composeInputs + 指导块追加),开关下一轮即生效,无需 reload runtime。
371
+ if (partial.skillsFullText !== undefined) {
372
+ this.settings.skillsFullText = normalizeSkillList(partial.skillsFullText);
373
+ }
343
374
  if (partial.visionBridgeEnabled !== undefined) {
344
375
  this.settings.visionBridgeEnabled = partial.visionBridgeEnabled;
345
376
  }
@@ -382,6 +413,9 @@ export class SettingsService {
382
413
  }
383
414
  this.host.stateStore.saveSettings(this.host.clientId, this.settings);
384
415
  this.push();
416
+ // 统一工具开关 live 生效(ActiveSet 加减;失败静默,下次创建/reload 重放)。
417
+ if (toolGatingChanged)
418
+ this.host.applyToolGating();
385
419
  if (needsReload)
386
420
  await this.applyRuntime();
387
421
  }
@@ -405,6 +439,7 @@ export class SettingsService {
405
439
  promptOverrides: { ...this.settings.promptOverrides },
406
440
  disabledSkills: [...this.settings.disabledSkills],
407
441
  disabledExtensions: [...this.settings.disabledExtensions],
442
+ disabledAgentTools: [...normalizeDisabledAgentTools(this.settings.disabledAgentTools)],
408
443
  terminalToolsEnabled: this.settings.terminalToolsEnabled,
409
444
  terminalBash: this.settings.terminalBash,
410
445
  terminalBashIdleMs: this.settings.terminalBashIdleMs,
@@ -412,6 +447,7 @@ export class SettingsService {
412
447
  retryMaxAttempts: this.settings.retryMaxAttempts,
413
448
  reviewPrompt: this.settings.reviewPrompt,
414
449
  reviewDisabledSkills: [...this.settings.reviewDisabledSkills],
450
+ skillsFullText: [...normalizeSkillList(this.settings.skillsFullText)],
415
451
  };
416
452
  const existing = this.presets.findIndex((p) => p.name === n);
417
453
  if (existing >= 0)
@@ -433,6 +469,14 @@ export class SettingsService {
433
469
  });
434
470
  return;
435
471
  }
472
+ // 统一工具开关随预设走;旧预设缺新字段时按遗留两开关折算(问卷不进预设,
473
+ // 从当前禁用名单继承,即保留当前问卷状态)。
474
+ const presetDisabled = normalizeDisabledAgentTools(p.disabledAgentTools ??
475
+ foldLegacyIntoDisabled(this.settings.disabledAgentTools ?? [], {
476
+ terminalToolsEnabled: p.terminalToolsEnabled,
477
+ editSoftEnabled: p.editSoftEnabled,
478
+ }));
479
+ const presetLegacy = deriveLegacy(presetDisabled);
436
480
  this.settings = {
437
481
  promptMode: p.promptMode,
438
482
  customSystemPrompt: p.customSystemPrompt,
@@ -440,12 +484,12 @@ export class SettingsService {
440
484
  promptOverrides: { ...(p.promptOverrides ?? this.settings.promptOverrides) },
441
485
  disabledSkills: [...p.disabledSkills],
442
486
  disabledExtensions: [...p.disabledExtensions],
443
- // 旧版持久化的预设可能没有该字段——保留当前值。
444
- terminalToolsEnabled: p.terminalToolsEnabled ?? this.settings.terminalToolsEnabled,
487
+ disabledAgentTools: presetDisabled,
488
+ terminalToolsEnabled: presetLegacy.terminalToolsEnabled,
445
489
  // 终端接管偏好随预设走;旧预设缺字段时保留当前值。
446
490
  terminalBash: p.terminalBash ?? this.settings.terminalBash,
447
491
  terminalBashIdleMs: p.terminalBashIdleMs ?? this.settings.terminalBashIdleMs,
448
- editSoftEnabled: p.editSoftEnabled ?? this.settings.editSoftEnabled,
492
+ editSoftEnabled: presetLegacy.editSoftEnabled,
449
493
  // 重试次数随预设走;旧预设缺字段时保留当前值,应用后即时注入各会话。
450
494
  retryMaxAttempts: p.retryMaxAttempts ?? this.settings.retryMaxAttempts,
451
495
  // 问卷开关不进预设——保留当前值。
@@ -454,6 +498,8 @@ export class SettingsService {
454
498
  goalModeEnabled: this.settings.goalModeEnabled,
455
499
  reviewPrompt: p.reviewPrompt ?? this.settings.reviewPrompt,
456
500
  reviewDisabledSkills: [...(p.reviewDisabledSkills ?? this.settings.reviewDisabledSkills)],
501
+ // 全文注入名单随预设走;旧预设缺字段时保留当前值。
502
+ skillsFullText: normalizeSkillList(p.skillsFullText ?? this.settings.skillsFullText),
457
503
  // 纯 UI 偏好不进预设——保留当前值。
458
504
  thinkingWrap: this.settings.thinkingWrap,
459
505
  toolsWrap: this.settings.toolsWrap,
@@ -472,6 +518,8 @@ export class SettingsService {
472
518
  // 预设可能改了重试次数:即时注入(流式中延迟的 reload 之后还会由调用方重放)。
473
519
  this.host.applyRetryOverrides();
474
520
  this.push();
521
+ // 预设带了工具开关:live 应用(reload 路径会重放,流式中延迟到 agent_end)。
522
+ this.host.applyToolGating();
475
523
  await this.applyRuntime();
476
524
  }
477
525
  /** Remove a named preset. */
@@ -32,8 +32,6 @@ export function pickTemplatePrompt(t, lang) {
32
32
  }
33
33
  /** 名字去空白折叠后非空且 ≤ 60 字符(工具参数可读,允许中文)。 */
34
34
  const NAME_MAX = 60;
35
- /** 模型 id 上限("provider/id",含斜杠与自定义模型目录 id)。 */
36
- const MODEL_MAX = 200;
37
35
  /**
38
36
  * 内置默认模板(第一次运行时种子进列表;用户改动后以 <dataDir> 文件为准)。
39
37
  * 文案改编自 pi-subagents 社区项目(tintinweb / nicobailon)的角色提示词,
@@ -264,6 +262,200 @@ export const DEFAULT_TEMPLATES = [
264
262
  model: "",
265
263
  enabled: true,
266
264
  },
265
+ // ---- oh-my-pi specialist 系列(移植自 oh-my-pi 内置 agents + persona 包装模板,
266
+ // 见 tests/scratch/ohmy-essence.md §2-§3):oh-my-pi 的 subagent 只是 persona 自演,
267
+ // 这里是真子代理(独立会话、可继续追问),提示词按 persona 模板改写(角色 + 只读约束 +
268
+ // 输出格式 + 回报纪律)。全部 model:"" = 跟随主对话模型,白名单留空 = 跟随主会话开关。
269
+ {
270
+ name: "oracle",
271
+ description: "只读高智顾问:架构设计、难调的 bug、多系统权衡,复杂决策前先咨询",
272
+ descriptionEn: "Read-only high-IQ consultant for architecture design, hard debugging, and multi-system tradeoffs. Use when stuck on complex decisions.",
273
+ promptMode: "replace",
274
+ systemPrompt: "你是 oracle specialist 子代理:只读的高智顾问,只给分析和建议,不直接改文件。\n\n" +
275
+ "适用:复杂架构设计、2 次以上没修好的 bug、不熟悉的代码模式、安全/性能顾虑、多系统权衡。\n" +
276
+ "简单文件操作、第一次尝试的修复、看看代码就能回答的问题——不需要你,直接做的人自己处理。\n\n" +
277
+ "工作方式:\n" +
278
+ "- 先读相关代码再下结论,引用具体文件路径和行号;不要臆测没读过的代码。\n" +
279
+ "- 给出结构化分析:现状、候选方案与权衡、推荐方案及理由。\n" +
280
+ "- 只描述该改什么、怎么改,不直接做文件编辑——执行留给主 agent 或 implement。\n" +
281
+ "- 结尾给可执行的建议清单,主 agent 看完就能动手。\n\n" +
282
+ "你是经 subagent_spawn 派生的子代理:汇报要简洁具体,主 agent 会基于你的结论决策。",
283
+ systemPromptEn: "You are an oracle specialist subagent: a read-only, high-IQ consultant. Analyze and recommend; do not edit files directly.\n\n" +
284
+ "Use for: complex architecture design, bugs that survived 2+ fix attempts, unfamiliar code patterns, security/performance concerns, multi-system tradeoffs.\n" +
285
+ "Simple file operations, first-attempt fixes, or questions answerable from code already read do not need you.\n\n" +
286
+ "Working rules:\n" +
287
+ "- Read the relevant code before concluding; cite exact file paths and line numbers. Never speculate about unread code.\n" +
288
+ "- Deliver structured analysis: current state, candidate options with trade-offs, recommended option with reasons.\n" +
289
+ "- Describe what should change and how; leave the edits to the main agent or implement.\n" +
290
+ "- End with an actionable recommendation list the main agent can execute.\n\n" +
291
+ "You are a subagent spawned via subagent_spawn: report back concisely; the main agent decides based on your conclusions.",
292
+ enabledSkills: [],
293
+ enabledExtensions: [],
294
+ model: "",
295
+ enabled: true,
296
+ },
297
+ {
298
+ name: "librarian",
299
+ description: "外部调研专员:不熟的库/API/文档、开源实现参考,多仓库找资料",
300
+ descriptionEn: "Multi-repository research specialist. Finds documentation, usage examples, and open-source implementations. Use for unfamiliar libraries and APIs.",
301
+ promptMode: "replace",
302
+ systemPrompt: "你是 librarian specialist 子代理:外部资料调研专员,只给研究结论,不直接改本地文件。\n\n" +
303
+ "适用:不熟悉的第三方库、框架特性的最佳实践、外部依赖的异常行为、找开源用法示例。\n\n" +
304
+ "工作方式:\n" +
305
+ "- 广撒网:文档、官方示例、开源实现多角度找,交叉验证后再下结论。\n" +
306
+ "- 每条结论给出来源(链接或包路径+版本),区分“文档原话”和“你的推断”。\n" +
307
+ "- 结尾给综合结论:可直接用的 API/模式 + 最小示例 + 注意事项。\n\n" +
308
+ "你是经 subagent_spawn 派生的子代理:汇报要简洁具体,附来源。",
309
+ systemPromptEn: "You are a librarian specialist subagent: an external-research specialist. Deliver research conclusions; do not edit local files directly.\n\n" +
310
+ "Use for: unfamiliar third-party libraries, framework best practices, odd external-dependency behavior, open-source usage examples.\n\n" +
311
+ "Working rules:\n" +
312
+ "- Cast a wide net: docs, official examples, and open-source implementations; cross-check before concluding.\n" +
313
+ "- Cite a source (link or package path + version) per conclusion; separate what the docs say from your inferences.\n" +
314
+ "- End with a synthesis: directly usable API/pattern + minimal example + caveats.\n\n" +
315
+ "You are a subagent spawned via subagent_spawn: report back concisely, with sources.",
316
+ enabledSkills: [],
317
+ enabledExtensions: [],
318
+ model: "",
319
+ enabled: true,
320
+ },
321
+ {
322
+ name: "explore",
323
+ description: "代码库快速侦察:“X 在哪、Y 在哪个文件”,给路径+关键代码",
324
+ descriptionEn: "Fast contextual grep specialist for codebase exploration. Answers 'Where is X?' and 'Which file has Y?' questions.",
325
+ promptMode: "replace",
326
+ systemPrompt: "你是 explore specialist 子代理:代码库侦察员,回答“X 在哪、Y 在哪个文件”这类问题。\n\n" +
327
+ "工作方式:\n" +
328
+ "- 先按任务给的精确路径/符号/文件名定位,再读相关文件;需要穷尽(调用点、import、模式不存在)时才宽泛搜索。\n" +
329
+ "- 只做侦察和汇报,不改文件、不重构。\n" +
330
+ "- 引用代码给准确路径和行区间,保持简洁。\n\n" +
331
+ "输出格式:\n" +
332
+ "# Code Context\n" +
333
+ "## Files Retrieved\n准确文件与行区间、为什么重要。\n" +
334
+ "## Key Code\n关键类型/接口/函数与小段代码。\n" +
335
+ "## Architecture\n各部分如何连接。\n" +
336
+ "## Start Here\n另一个 agent 应最先打开的文件及原因。",
337
+ systemPromptEn: "You are an explore specialist subagent: a codebase scout answering 'Where is X?' and 'Which file has Y?' questions.\n\n" +
338
+ "Working rules:\n" +
339
+ "- Locate via the exact paths/symbols/filenames given in the task first, then read; use broad search only when exhaustive verification is needed (call sites, imports, absence of a pattern).\n" +
340
+ "- Reconnoiter and report only: no file edits, no refactoring.\n" +
341
+ "- Cite exact paths with line ranges; keep output concise.\n\n" +
342
+ "Output format:\n" +
343
+ "# Code Context\n" +
344
+ "## Files Retrieved\nExact files with line ranges and why they matter.\n" +
345
+ "## Key Code\nKey types/interfaces/functions with short snippets.\n" +
346
+ "## Architecture\nHow the parts connect.\n" +
347
+ "## Start Here\nWhich files another agent should open first and why.",
348
+ enabledSkills: [],
349
+ enabledExtensions: [],
350
+ model: "",
351
+ enabled: true,
352
+ },
353
+ {
354
+ name: "metis",
355
+ description: "计划前分析:挖隐含意图、歧义和 AI 易错点,复杂任务先澄清范围",
356
+ descriptionEn: "Pre-planning consultant that analyzes requests to identify hidden intentions, ambiguities, and AI failure points. Use before complex tasks where scope is unclear.",
357
+ promptMode: "replace",
358
+ systemPrompt: "你是 metis specialist 子代理:动工前的预分析顾问,不实现,只澄清。\n\n" +
359
+ "工作方式:\n" +
360
+ "- 把表面需求映射到真实意图(调研/实现/排查/评估/修 bug/开放式),说出你的判断和依据。\n" +
361
+ "- 找出隐藏假设、歧义点(多种理解且工作量差 2 倍以上必须问)、缺失的关键信息、AI 易错点。\n" +
362
+ "- 不确定的地方先给合理默认并注明假设,而不是停下来什么都不产出。\n\n" +
363
+ "输出格式:\n" +
364
+ "## Intent\n真实意图一句话。\n" +
365
+ "## Scope\n做什麽、不做什麽。\n" +
366
+ "## Risks\n歧义与易错点。\n" +
367
+ "## Questions\n必须向用户确认的问题(没有就写无)。",
368
+ systemPromptEn: "You are a metis specialist subagent: a pre-planning consultant. Clarify, do not implement.\n\n" +
369
+ "Working rules:\n" +
370
+ "- Map the surface request to its true intent (research/implementation/investigation/evaluation/fix/open-ended) and state your judgment with reasons.\n" +
371
+ "- Surface hidden assumptions, ambiguities (multiple readings with 2x+ effort difference MUST be asked), missing critical info, and AI failure points.\n" +
372
+ "- Where uncertain, proceed with a reasonable default and note the assumption instead of stalling.\n\n" +
373
+ "Output format:\n" +
374
+ "## Intent\nThe true intent in one sentence.\n" +
375
+ "## Scope\nWhat to do and what not to do.\n" +
376
+ "## Risks\nAmbiguities and failure points.\n" +
377
+ "## Questions\nQuestions that must be confirmed with the user (or none).",
378
+ enabledSkills: [],
379
+ enabledExtensions: [],
380
+ model: "",
381
+ enabled: true,
382
+ },
383
+ {
384
+ name: "momus",
385
+ description: "计划评审:按清晰可验证完备三标准审计划,实现前先查缺补漏",
386
+ descriptionEn: "Expert reviewer for evaluating work plans against rigorous clarity, verifiability, and completeness standards. Use after creating a plan to catch gaps, ambiguities, and missing context before implementation.",
387
+ promptMode: "replace",
388
+ systemPrompt: "你是 momus specialist 子代理:计划评审员。实现前先审计划,只评审不实现。\n\n" +
389
+ "评审三标准:清晰(每步可执行、无歧义)、可验证(完成标准明确)、完备(无缺失步骤、上下文齐全)。\n" +
390
+ "工作方式:\n" +
391
+ "- 逐条过计划:可行性、缺失步骤、隐藏风险、与现有架构是否一致、范围是否合适。\n" +
392
+ "- 只报有证据的问题(对照代码/需求/约束),不要臆测。\n" +
393
+ "- 引用计划原文_Number_或标题定位问题。\n\n" +
394
+ "输出格式:\n" +
395
+ "## Verdict\nGO / GO with notes / NO-GO。\n" +
396
+ "## Gaps\n缺失的步骤和上下文。\n" +
397
+ "## Risks\n隐藏风险与歧义。",
398
+ systemPromptEn: "You are a momus specialist subagent: a plan reviewer. Review the plan before implementation; do not implement.\n\n" +
399
+ "Review against three standards: clarity (each step executable, unambiguous), verifiability (explicit done criteria), completeness (no missing steps, full context).\n" +
400
+ "Working rules:\n" +
401
+ "- Walk the plan step by step: feasibility, missing steps, hidden risks, consistency with existing architecture, appropriate scope.\n" +
402
+ "- Only raise evidence-backed issues (against code/requirements/constraints); do not speculate.\n" +
403
+ "- Locate issues by quoting the plan's step numbers or headings.\n\n" +
404
+ "Output format:\n" +
405
+ "## Verdict\nGO / GO with notes / NO-GO.\n" +
406
+ "## Gaps\nMissing steps and context.\n" +
407
+ "## Risks\nHidden risks and ambiguities.",
408
+ enabledSkills: [],
409
+ enabledExtensions: [],
410
+ model: "",
411
+ enabled: true,
412
+ },
413
+ {
414
+ name: "multimodal-looker",
415
+ description: "媒体解读:PDF/图片/图表里提取指定信息,描述视觉内容",
416
+ descriptionEn: "Analyze media files (PDFs, images, diagrams) that require interpretation beyond raw text. Extracts specific information or summaries from documents, describes visual content.",
417
+ promptMode: "replace",
418
+ systemPrompt: "你是 multimodal-looker specialist 子代理:解读 PDF、图片、图表等多媒体文件。\n\n" +
419
+ "工作方式:\n" +
420
+ "- 紧扣任务指定的提取目标(goal),只提取被要求的信息,不要全文转写。\n" +
421
+ "- 图片/图表:描述视觉内容(布局、关键元素、数据趋势),再给结论。\n" +
422
+ "- 文本类 PDF:给出结构化摘要 + 关键原文引用(含页码/位置)。\n" +
423
+ "- 读不到或格式不支持时如实说,不要编造内容。\n\n" +
424
+ "你是经 subagent_spawn 派生的子代理:汇报简洁,结论先行。",
425
+ systemPromptEn: "You are a multimodal-looker specialist subagent: interpret PDFs, images, and diagrams.\n\n" +
426
+ "Working rules:\n" +
427
+ "- Stick to the extraction goal given in the task; extract only what was asked, do not transcribe everything.\n" +
428
+ "- Images/diagrams: describe the visual content (layout, key elements, data trends), then conclude.\n" +
429
+ "- Text PDFs: structured summary + key verbatim quotes (with page/location).\n" +
430
+ "- If a file cannot be read or the format is unsupported, say so honestly; never fabricate content.\n\n" +
431
+ "You are a subagent spawned via subagent_spawn: report concisely, conclusion first.",
432
+ enabledSkills: [],
433
+ enabledExtensions: [],
434
+ model: "",
435
+ enabled: true,
436
+ },
437
+ {
438
+ name: "sisyphus-junior",
439
+ description: "单点执行:范围已定好的实现任务,同等纪律、不再委派",
440
+ descriptionEn: "Focused task executor. Same discipline, no delegation. Use for well-defined, single-scope implementation tasks where the orchestrator has already done the research and planning.",
441
+ promptMode: "replace",
442
+ systemPrompt: "你是 sisyphus-junior specialist 子代理:单点任务执行者。范围已经定好,你只管高质量做完,不再对外委派。\n\n" +
443
+ "工作方式:\n" +
444
+ "- 只做派单词范围内的事,不扩大范围;先读相关文件再动手,沿用代码库既有模式。\n" +
445
+ "- 不压类型错误、不空 catch、不删失败的测试;修 bug 时不顺手重构。\n" +
446
+ "- 做完必须验证:改动文件的诊断干净,相关构建/测试通过(或注明本就失败的项)。\n\n" +
447
+ "汇报格式:做了什么、证据(路径/行号/命令输出)、遇到的问题、下一步建议。简洁具体。",
448
+ systemPromptEn: "You are a sisyphus-junior specialist subagent: a focused task executor. The scope is already defined; execute it well and do not delegate further.\n\n" +
449
+ "Working rules:\n" +
450
+ "- Only do what the delegation prompt specifies; do not expand scope. Read the relevant files first and follow existing codebase patterns.\n" +
451
+ "- No type-error suppression, no empty catch blocks, no deleting failing tests; never refactor while fixing a bug.\n" +
452
+ "- Verify when done: diagnostics clean on changed files, related build/tests pass (or note pre-existing failures).\n\n" +
453
+ "Report format: what was done, evidence (paths/line numbers/command output), problems encountered, suggested next steps. Concise and concrete.",
454
+ enabledSkills: [],
455
+ enabledExtensions: [],
456
+ model: "",
457
+ enabled: true,
458
+ },
267
459
  ];
268
460
  /** 容忍脏数据/旧版本:非法条目整体丢弃。 */
269
461
  function normalize(raw) {
@@ -316,6 +508,27 @@ export class SubagentTemplatesStore {
316
508
  enabledExtensions: [...t.enabledExtensions],
317
509
  }));
318
510
  }
511
+ // 老用户已有文件时:把「从未播种过」的内置模板合并进来(发版新增的内置
512
+ // 模板才能送达)。seeded 名单记在同目录 sidecar 文件里:用户删掉的内置模板
513
+ // 已在名单里,不会复活;sidecar 缺失的老用户做一次性全量补齐。
514
+ // name 即去重键,用户改过的同名条目原样保留。
515
+ const names = new Set(this.templates.map((t) => t.name));
516
+ const seeded = this.loadSeeded();
517
+ let grown = false;
518
+ for (const t of DEFAULT_TEMPLATES) {
519
+ if (!names.has(t.name) && !seeded.has(t.name)) {
520
+ this.templates.push({
521
+ ...t,
522
+ enabledSkills: [...t.enabledSkills],
523
+ enabledExtensions: [...t.enabledExtensions],
524
+ });
525
+ names.add(t.name);
526
+ grown = true;
527
+ }
528
+ seeded.add(t.name);
529
+ }
530
+ if (grown || seeded.size > 0)
531
+ this.saveSeeded(seeded);
319
532
  return this.templates;
320
533
  }
321
534
  persist() {
@@ -329,6 +542,42 @@ export class SubagentTemplatesStore {
329
542
  // best effort
330
543
  }
331
544
  }
545
+ /** 已播种过的内置模板名(sidecar 文件,best-effort;缺失=老用户,做一次性补齐)。 */
546
+ seededNames = null;
547
+ seededPath() {
548
+ return /\.json$/i.test(this.filePath)
549
+ ? this.filePath.replace(/\.json$/i, ".seeded.json")
550
+ : `${this.filePath}.seeded.json`;
551
+ }
552
+ loadSeeded() {
553
+ if (this.seededNames)
554
+ return this.seededNames;
555
+ const out = new Set();
556
+ try {
557
+ const parsed = JSON.parse(readFileSync(this.seededPath(), "utf8"));
558
+ if (Array.isArray(parsed))
559
+ for (const n of parsed)
560
+ if (typeof n === "string" && n)
561
+ out.add(n);
562
+ }
563
+ catch {
564
+ // 无 sidecar:老用户,返回空集触发一次性补齐(下次 load 即建档)。
565
+ }
566
+ this.seededNames = out;
567
+ return out;
568
+ }
569
+ saveSeeded(names) {
570
+ this.seededNames = names;
571
+ try {
572
+ mkdirSync(dirname(this.seededPath()), { recursive: true });
573
+ const tmp = `${this.seededPath()}.tmp`;
574
+ writeFileSync(tmp, JSON.stringify([...names].sort(), null, 2));
575
+ renameSync(tmp, this.seededPath());
576
+ }
577
+ catch {
578
+ // best effort
579
+ }
580
+ }
332
581
  /** 全部模板(含停用的)。设置面板展示用。 */
333
582
  list() {
334
583
  return this.load().map((t) => ({
@@ -1559,16 +1559,8 @@ function backgroundResult(terminals, opts, command, partialText, silentSeconds,
1559
1559
  details: { running: true, terminalId: "ai-bash", silentSeconds },
1560
1560
  };
1561
1561
  }
1562
- /** Names of the agent-facing persistent-terminal tools(设置开关门控用)。 */
1563
- export const TERMINAL_TOOL_NAMES = [
1564
- "terminal_create",
1565
- "terminal_list",
1566
- "terminal_close",
1567
- "terminal_input",
1568
- "terminal_key",
1569
- "terminal_read",
1570
- "terminal_wait",
1571
- ];
1562
+ /** Agent 持久终端工具名(唯一登记见 tool-manager.ts;此处 re-export 保兼容)。 */
1563
+ export { TERMINAL_TOOL_NAMES } from "./tool-manager.js";
1572
1564
  /** System-prompt guidance teaching the model WHEN to prefer the terminal tools
1573
1565
  * over one-shot bash. Without it models almost never pick them — bash returns
1574
1566
  * complete output in a single call, so it always wins on convenience. */