pi-web-ui 0.76.0 → 0.77.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.
@@ -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. */
@@ -0,0 +1,217 @@
1
+ /**
2
+ * tool-manager.ts — Agent 工具开关的唯一事实源 + 统一出入口(tool_manage)。
3
+ *
4
+ * 背景:工具启用散在 4 种写法里——applyToolGating 只会 terminal_* + edit_soft、
5
+ * “子代理系列”/delegate_task/ask_user_question/markers_list 注册即常驻(无开关)、
6
+ * skills/extensions 走 resourceLoader 过滤(要 reload)、terminalBash 等是行为
7
+ * 分支。本模块只收 **ActiveSet 层**(SDK customTools 经
8
+ * getActiveToolNames/setActiveToolsByName 的启用/禁用,live 生效、无需 reload);
9
+ * skills/extensions(资源过滤,另一生命周期)与行为开关不进此表。
10
+ *
11
+ * 持久化只有 `ClientSettings.disabledAgentTools: string[]`(禁用的工具名);
12
+ * 旧的 terminalToolsEnabled/editSoftEnabled/questionnaireEnabled 作为遗留别名
13
+ * 保留(协议兼容),由本模块的 legacy* helper 双向同步。
14
+ *
15
+ * 纯模块:零 node 依赖(不 import 任何 server 模块),前端可直接 import
16
+ * (vite 构建不断),单测零开销。
17
+ */
18
+ /** 持久终端工具(定义见 terminals.ts,工具名在此唯一登记)。 */
19
+ export const TERMINAL_TOOL_NAMES = [
20
+ "terminal_create",
21
+ "terminal_list",
22
+ "terminal_close",
23
+ "terminal_input",
24
+ "terminal_key",
25
+ "terminal_read",
26
+ "terminal_wait",
27
+ ];
28
+ /** 第一方子代理工具(定义见 subagents.ts,逐个可关)。 */
29
+ export const SUBAGENT_TOOL_NAMES = [
30
+ "subagent_spawn",
31
+ "subagent_get_result",
32
+ "subagent_steer",
33
+ "subagent_list",
34
+ "subagent_stop",
35
+ "subagent_wait_all",
36
+ "subagent_templates",
37
+ ];
38
+ /** 独立宽松编辑工具(定义见 edit-soft-tool.ts)。 */
39
+ export const EDIT_SOFT_TOOL_NAME = "edit_soft";
40
+ /** 结构化派单工具(定义见 delegate-task.ts,执行体复用子代理 spawn 通道)。 */
41
+ export const DELEGATE_TASK_TOOL_NAME = "delegate_task";
42
+ /** 问卷提问工具(定义见 agent-service.ts makeAskUserQuestionTool)。 */
43
+ export const ASK_USER_QUESTION_TOOL_NAME = "ask_user_question";
44
+ /** 任务列表只读查询工具(定义见 agent-service.ts makeMarkersListTool;只服务 todo)。
45
+ * 曾用名 markers_list(名过其实,已迁移,见 normalizeDisabledAgentTools)。 */
46
+ export const MARKERS_LIST_TOOL_NAME = "todo_list";
47
+ /** 旧工具名(持久化迁移用;新代码一律用 MARKERS_LIST_TOOL_NAME)。 */
48
+ export const LEGACY_MARKERS_LIST_TOOL_NAME = "markers_list";
49
+ /** 可开关的 Agent 工具总目录(共 18 个;bash 本体与 SDK 内置 edit/read
50
+ * 不进目录——关了 agent 就残了,不给关)。 */
51
+ export const AGENT_TOOL_CATALOG = [
52
+ ...TERMINAL_TOOL_NAMES.map((name) => ({
53
+ name,
54
+ group: "terminal",
55
+ defaultOn: false,
56
+ dshVisible: true,
57
+ })),
58
+ ...SUBAGENT_TOOL_NAMES.map((name) => ({
59
+ name,
60
+ group: "subagent",
61
+ defaultOn: true,
62
+ dshVisible: false,
63
+ })),
64
+ { name: EDIT_SOFT_TOOL_NAME, group: "other", defaultOn: false, dshVisible: false },
65
+ { name: DELEGATE_TASK_TOOL_NAME, group: "other", defaultOn: true, dshVisible: false },
66
+ { name: ASK_USER_QUESTION_TOOL_NAME, group: "other", defaultOn: true, dshVisible: true },
67
+ { name: MARKERS_LIST_TOOL_NAME, group: "other", defaultOn: true, dshVisible: true },
68
+ ];
69
+ const KNOWN_NAMES = new Set(AGENT_TOOL_CATALOG.map((t) => t.name));
70
+ /** 是否为本表登记的可开关工具(未知名一律 false,不抛错)。 */
71
+ export function isKnownAgentTool(name) {
72
+ return KNOWN_NAMES.has(name);
73
+ }
74
+ /** 归一化禁用名单:非数组回落默认(= 默认关的那些);数组则只保留已知工具名
75
+ * (去重;未知名丢弃,防旧文件/手写脏数据污染)。 */
76
+ export function normalizeDisabledAgentTools(v) {
77
+ if (!Array.isArray(v))
78
+ return defaultDisabledAgentTools();
79
+ const out = [];
80
+ for (const x of v) {
81
+ // 旧名迁移:markers_list → todo_list(改名前已关闭的用户保持关闭)。
82
+ const name = x === LEGACY_MARKERS_LIST_TOOL_NAME ? MARKERS_LIST_TOOL_NAME : x;
83
+ if (typeof name === "string" && KNOWN_NAMES.has(name) && !out.includes(name))
84
+ out.push(name);
85
+ }
86
+ return out;
87
+ }
88
+ /** 默认禁用名单(= 目录里 defaultOn=false 的那些)。 */
89
+ export function defaultDisabledAgentTools() {
90
+ return AGENT_TOOL_CATALOG.filter((t) => !t.defaultOn).map((t) => t.name);
91
+ }
92
+ /** 单个工具是否启用(禁用名单里没有 = 启用)。 */
93
+ export function isAgentToolEnabled(name, disabled) {
94
+ return !disabled.includes(name);
95
+ }
96
+ /**
97
+ * 统一出入口 tool_manage:开关任意一个已登记的工具(live 生效,无需 reload;
98
+ * 工具仍留在注册表,重开可直接加回)。未知工具名返回 false(不抛错,
99
+ * 调用方据此给 AI/用户报错);session 未就绪同样返回 false。
100
+ */
101
+ export function setAgentToolEnabled(session, name, on) {
102
+ if (!isKnownAgentTool(name))
103
+ return false;
104
+ try {
105
+ const names = new Set(session.getActiveToolNames());
106
+ if (on)
107
+ names.add(name);
108
+ else
109
+ names.delete(name);
110
+ session.setActiveToolsByName([...names]);
111
+ return true;
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ }
117
+ /** 批量版(组头全开/全关用;含未知名时照常处理已知部分,返回实际处理数)。 */
118
+ export function setAgentToolsEnabled(session, names, on) {
119
+ const known = names.filter(isKnownAgentTool);
120
+ if (known.length === 0)
121
+ return 0;
122
+ try {
123
+ const active = new Set(session.getActiveToolNames());
124
+ for (const n of known) {
125
+ if (on)
126
+ active.add(n);
127
+ else
128
+ active.delete(n);
129
+ }
130
+ session.setActiveToolsByName([...active]);
131
+ return known.length;
132
+ }
133
+ catch {
134
+ return 0;
135
+ }
136
+ }
137
+ /**
138
+ * 全量重放(创建会话 / reload 后 / 设置变更后调):按禁用名单把目录内工具
139
+ * 逐个加回或剔除;目录外的工具(bash/SDK 内置/插件工具)原样不动。
140
+ * Session 未就绪时静默跳过(下次创建/reload 会再应用)。
141
+ */
142
+ export function applyAgentToolsGating(session, disabled) {
143
+ try {
144
+ const off = new Set(disabled);
145
+ const names = new Set(session.getActiveToolNames());
146
+ for (const t of AGENT_TOOL_CATALOG) {
147
+ if (off.has(t.name))
148
+ names.delete(t.name);
149
+ else
150
+ names.add(t.name);
151
+ }
152
+ session.setActiveToolsByName([...names]);
153
+ }
154
+ catch {
155
+ // Session 未就绪——下次创建/reload 会再应用。
156
+ }
157
+ }
158
+ /**
159
+ * 旧存档迁移:已有新字段直接归一化;否则按遗留三开关折算
160
+ * (语义与改动前一致:terminal/edit 未设/关 = 禁用对应组;问卷未设/开 = 启用)。
161
+ */
162
+ export function legacyToDisabled(s) {
163
+ if (Array.isArray(s.disabledAgentTools))
164
+ return normalizeDisabledAgentTools(s.disabledAgentTools);
165
+ const off = [];
166
+ if (s.terminalToolsEnabled !== true)
167
+ off.push(...TERMINAL_TOOL_NAMES);
168
+ if (s.editSoftEnabled !== true)
169
+ off.push(EDIT_SOFT_TOOL_NAME);
170
+ if (s.questionnaireEnabled === false)
171
+ off.push(ASK_USER_QUESTION_TOOL_NAME);
172
+ return normalizeDisabledAgentTools(off);
173
+ }
174
+ /** 由禁用名单推导遗留三开关(协议兼容用;终端组按“全开才算开”的全有/全无视图)。 */
175
+ export function deriveLegacy(disabled) {
176
+ const off = new Set(disabled);
177
+ return {
178
+ terminalToolsEnabled: TERMINAL_TOOL_NAMES.every((n) => !off.has(n)),
179
+ editSoftEnabled: !off.has(EDIT_SOFT_TOOL_NAME),
180
+ questionnaireEnabled: !off.has(ASK_USER_QUESTION_TOOL_NAME),
181
+ };
182
+ }
183
+ /**
184
+ * 遗留单开关写入时折回新字段(只动开关覆盖的组,其余条目原样保留):
185
+ * 传 true = 把该组从禁用名单移除,false = 加入,未传 = 不动。
186
+ */
187
+ export function foldLegacyIntoDisabled(current, legacy) {
188
+ const next = new Set(normalizeDisabledAgentTools(current));
189
+ const applyGroup = (names, v) => {
190
+ if (v === undefined)
191
+ return;
192
+ for (const n of names) {
193
+ if (v)
194
+ next.delete(n);
195
+ else
196
+ next.add(n);
197
+ }
198
+ };
199
+ applyGroup(TERMINAL_TOOL_NAMES, legacy.terminalToolsEnabled);
200
+ applyGroup([EDIT_SOFT_TOOL_NAME], legacy.editSoftEnabled);
201
+ applyGroup([ASK_USER_QUESTION_TOOL_NAME], legacy.questionnaireEnabled);
202
+ return [...next];
203
+ }
204
+ /**
205
+ * 门控实效名单:新字段 + 问卷别名合并(问卷关 = ask 工具必关,双保险;
206
+ * 两处平时由 set() 同步一致,合并只防陈旧会话/旧客户端的半边状态)。
207
+ */
208
+ export function effectiveDisabledAgentTools(s) {
209
+ const next = new Set(legacyToDisabled({ ...s, disabledAgentTools: s.disabledAgentTools }));
210
+ if (s.questionnaireEnabled === false)
211
+ next.add(ASK_USER_QUESTION_TOOL_NAME);
212
+ return [...next];
213
+ }
214
+ /** 终端使用引导是否注入(组内有任一工具启用才教 AI 用,否则就是教不存在的工具)。 */
215
+ export function isTerminalGuidanceOn(disabled) {
216
+ return TERMINAL_TOOL_NAMES.some((n) => !disabled.includes(n));
217
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.76.0",
3
+ "version": "0.77.0",
4
4
  "description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -0,0 +1,6 @@
1
+ import{a as l,j as n}from"./markdown-Cpo0pNcR.js";import{u as P,b as O,T as M,a as p,c as W,F as Y,d as z,e as Z,f as H,g as ee,h as ne,i as te,j as se,r as ae}from"./index-BsATrxLx.js";import{D as re,o as ie}from"./xterm-B96xOxS9.js";import"./react-CtudoG1_.js";function le(t){let c=null;return{clean:t.replace(/\r?\n?\[pi-term-exit:(-?\d+)\]\r?\n?/g,(h,u)=>(c=Number(u),`\r
2
+ `)).replace(/\r?\n?\x1b\[90m\[(?:进程已退出,退出码 |Process exited with code )-?\d+\]\x1b\[0m\r?\n?/g,`\r
3
+ `),exitCode:c}}function ce({conversationId:t,terminalId:c,command:s,cwd:h,title:u,active:w,running:f,exitCode:k,register:g}){const C=l.useRef(null),j=l.useRef(null),{locale:o}=P(),y=l.useRef(o);y.current=o;const E=s?JSON.stringify(s):"";l.useEffect(()=>{const m=C.current;if(!m)return;const r=new re({theme:O(),fontFamily:'"SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace',fontSize:13,cursorBlink:!0,scrollback:8e3}),v=new ie;r.loadAddon(v),r.open(m),j.current={term:r,fit:v},w&&r.focus();const b=()=>{r.options.theme=O()};window.addEventListener(M,b),r.attachCustomKeyEventHandler(d=>{if(d.type!=="keydown")return!0;const S=d.key?.toLowerCase();if((d.ctrlKey||d.metaKey)&&S==="v")return!1;if(d.ctrlKey&&!d.shiftKey&&!d.altKey&&S==="c"&&r.hasSelection()){const D=r.textarea;return D&&(D.value=r.getSelection(),D.select()),!1}return!0});const _=g(t,c,{write:d=>r.write(le(d).clean),dispose:()=>r.dispose()}),$=()=>{try{v.fit(),p({type:"terminal_resize",terminalId:c,conversationId:t,cols:r.cols,rows:r.rows})}catch{}},B=requestAnimationFrame(()=>{try{v.fit()}catch{}s?p({type:"run_command",terminalId:c,conversationId:t,command:s,cols:r.cols,rows:r.rows}):p({type:"terminal_create",terminalId:c,title:u,locale:y.current,conversationId:t,cwd:h,cols:r.cols,rows:r.rows})}),R=r.onData(d=>{p({type:"terminal_input",terminalId:c,conversationId:t,data:d})});let T=null;return typeof ResizeObserver<"u"&&(T=new ResizeObserver(()=>{m.offsetWidth>0&&m.offsetHeight>0&&$()}),T.observe(m)),()=>{cancelAnimationFrame(B),R.dispose(),window.removeEventListener(M,b),T?.disconnect(),_(),r.dispose(),j.current=null}},[t,c,E,g]),l.useEffect(()=>{if(!w)return;const m=requestAnimationFrame(()=>{const r=j.current;if(r){try{r.fit.fit(),p({type:"terminal_resize",terminalId:c,conversationId:t,cols:r.term.cols,rows:r.term.rows})}catch{}r.term.focus()}});return()=>cancelAnimationFrame(m)},[w]);const{t:F}=P(),x=l.useRef(void 0);return l.useEffect(()=>{if(f===x.current||(x.current=f,f!==!1))return;const m=j.current;m&&m.term.write(`\r
4
+ \x1B[90m${F("exitBanner",{code:k??""})}\x1B[0m\r
5
+ `)},[f,c,k]),n.jsx("div",{ref:C,className:`term-xterm ${w?"":"hidden"}`})}const q={name:"",command:"",cwd:"${pwd}"};function pe({chat:t,terminal:c}){const s=W(),[h,u]=l.useState(null),[w,f]=l.useState(!1),[k,g]=l.useState(!1),[C,j]=l.useState(null),[o,y]=l.useState(q),[E,F]=l.useState(null),x=l.useRef(null),[m,r]=l.useState(!0),[v,b]=l.useState(null),[_,$]=l.useState("");l.useEffect(()=>{t.terminals.length===0?u(null):t.terminals.some(e=>e.id===h)||u(t.terminals[t.terminals.length-1].id)},[t.terminals,h]),l.useEffect(()=>{t.terminalActiveId&&(u(t.terminalActiveId),f(!1))},[t.terminalActiveId]),l.useEffect(()=>()=>{x.current&&clearTimeout(x.current)},[]);const B=t.terminals.filter(e=>!e.agentBash),R=t.terminals.filter(e=>e.agentBash),T=e=>{if(!t.ready)return;const a=ae(),i=t.activeConversationId||t.state?.conversationId||"";c.create({...e,id:a,conversationId:i,cols:e.cols??80,rows:e.rows??24,running:!0,exitCode:null}),u(a),f(!1)},d=()=>T({title:s("terminalTitle",{n:B.length+1}),cwd:t.state?.cwd??""}),S=e=>{const a=e.name||e.command,i=t.terminals.find(N=>N.title===a);if(i){c.restart(i.id),u(i.id),p({type:"run_command",terminalId:i.id,conversationId:i.conversationId,command:e,cols:80,rows:24});return}T({title:a,cwd:t.state?.cwd??"",command:e})},D=e=>{const a=t.terminals.find(i=>i.id===e);if(a&&p({type:"terminal_kill",terminalId:e,conversationId:a.conversationId}),c.close(e),h===e){const i=t.terminals.filter(N=>N.id!==e);u(i.length>0?i[i.length-1].id:null)}},I=e=>n.jsxs("div",{className:`term-tab ${e.id===h?"active":""}`,children:[n.jsxs("button",{type:"button",className:"term-tab-main",title:`${e.cwd}${e.command?`
6
+ > ${e.command.command}`:""}`,onClick:()=>{v||(u(e.id),f(!1))},children:[n.jsx("span",{className:`term-tab-dot ${e.running?"run":"exit"}`}),n.jsxs("span",{className:"term-tab-title",children:[v===e.id?n.jsx("input",{autoFocus:!0,className:"term-tab-rename-input",value:_,placeholder:e.title,onClick:a=>a.stopPropagation(),onChange:a=>$(a.target.value),onKeyDown:a=>{if(a.stopPropagation(),a.key==="Enter"&&!a.nativeEvent.isComposing){const i=_.trim();i&&p({type:"rename_terminal",terminalId:e.id,conversationId:e.conversationId,title:i}),b(null)}else a.key==="Escape"&&b(null)},onBlur:()=>b(null)}):e.title,!e.running&&n.jsx("span",{className:"term-tab-exit",children:s("exited",{code:e.exitCode===null?"":` ${e.exitCode}`})})]})]}),n.jsx("button",{type:"button",className:"term-tab-close term-tab-rename",title:s("renameTerminal"),onClick:a=>{a.stopPropagation(),$(e.title),b(e.id)},children:n.jsx(H,{})}),n.jsx("button",{type:"button",className:"term-tab-close",title:s("closeTerminal"),onClick:()=>D(e.id),children:n.jsx(se,{})})]},e.id),G=()=>{g(!0),j(null),y(q)},L=e=>{const a=t.commands[e];a&&(g(!1),j(e),y({name:a.name,command:a.command,cwd:a.cwd??""}))},K=()=>{g(!1),j(null)},A=()=>{const e=o.name.trim(),a=o.command.trim();if(!e||!a)return;const i=o.cwd.trim(),N={name:e,command:a,cwd:i||void 0},Q=k?[...t.commands,N]:C!==null?t.commands.map((U,V)=>V===C?N:U):t.commands;p({type:"save_commands",commands:Q}),K()},J=e=>{if(E===e){const a=t.commands.filter((i,N)=>N!==e);p({type:"save_commands",commands:a}),F(null),x.current&&clearTimeout(x.current)}else F(e),x.current&&clearTimeout(x.current),x.current=setTimeout(()=>F(null),2500)},X=k||C!==null;return n.jsxs("div",{className:"terminal-view",children:[n.jsxs("aside",{className:`term-side term-commands ${w?"open":""}`,children:[n.jsxs("div",{className:"panel-header",children:[n.jsx("span",{className:"panel-title",children:s("commands")}),n.jsxs("div",{className:"panel-header-actions",children:[n.jsx("button",{type:"button",className:"panel-refresh",title:s("rerun"),onClick:()=>p({type:"list_commands"}),children:n.jsx(Y,{})}),n.jsx("button",{type:"button",className:"panel-new",title:s("newCommand"),onClick:G,children:n.jsx(z,{})})]})]}),n.jsx("div",{className:"panel-body",children:X?n.jsxs("div",{className:"cmd-form",children:[n.jsx("label",{htmlFor:"cmd-name",children:s("name")}),n.jsx("input",{id:"cmd-name",className:"cmd-input",value:o.name,placeholder:s("exampleName"),autoFocus:!0,onChange:e=>y({...o,name:e.target.value})}),n.jsx("label",{htmlFor:"cmd-command",children:s("command")}),n.jsx("input",{id:"cmd-command",className:"cmd-input",value:o.command,placeholder:s("exampleCommand"),onChange:e=>y({...o,command:e.target.value}),onKeyDown:e=>{e.key==="Enter"&&!e.nativeEvent.isComposing&&A()}}),n.jsxs("label",{htmlFor:"cmd-cwd",children:[s("directory")," ",n.jsx("span",{className:"cmd-hint",children:s("cwdHint")})]}),n.jsx("input",{id:"cmd-cwd",className:"cmd-input",value:o.cwd,placeholder:"${pwd}",onChange:e=>y({...o,cwd:e.target.value}),onKeyDown:e=>{e.key==="Enter"&&!e.nativeEvent.isComposing&&A()}}),n.jsxs("div",{className:"cmd-form-actions",children:[n.jsx("button",{type:"button",className:"btn",onClick:K,children:s("cancel")}),n.jsx("button",{type:"button",className:"btn primary",disabled:!o.name.trim()||!o.command.trim(),onClick:A,children:s("save")})]})]}):n.jsxs(n.Fragment,{children:[t.commands.length===0&&n.jsx("div",{className:"panel-empty",children:s("noCommands")}),t.commands.map((e,a)=>n.jsxs("div",{className:"cmd-item",children:[n.jsx("button",{type:"button",className:"cmd-run",title:s("clickToRun"),onClick:()=>S(e),children:n.jsx(Z,{})}),n.jsxs("button",{type:"button",className:"cmd-main",title:s("clickToRun"),onClick:()=>S(e),children:[n.jsx("span",{className:"cmd-name",children:e.name}),n.jsx("span",{className:"cmd-command",children:e.command}),e.cwd&&n.jsx("span",{className:"cmd-cwd",children:e.cwd})]}),n.jsx("button",{type:"button",className:"cmd-act",title:s("edit"),onClick:()=>L(a),children:n.jsx(H,{})}),n.jsx("button",{type:"button",className:`cmd-act del ${E===a?"confirm":""}`,title:s("delete"),onClick:()=>J(a),children:E===a?s("confirmQ"):n.jsx(ee,{})})]},a))]})}),n.jsxs("div",{className:"term-tabs-block",children:[n.jsxs("div",{className:"panel-header",children:[n.jsx("span",{className:"panel-title",children:s("terminal")}),n.jsx("button",{type:"button",className:"panel-new",title:s("newTerminal"),onClick:d,children:n.jsx(z,{})})]}),n.jsxs("div",{className:"panel-body",children:[t.terminals.length===0&&n.jsx("div",{className:"panel-empty",children:s("noTerminal")}),B.map(I),R.length>0&&n.jsxs("div",{className:"term-folder",children:[n.jsxs("button",{type:"button",className:`term-folder-header ${m?"open":""}`,title:s("aiBashGroup"),onClick:()=>r(e=>!e),children:[n.jsx("span",{className:"term-folder-caret",children:m?"▾":"▸"}),n.jsx("span",{className:"term-folder-title",children:s("aiBashGroup")}),n.jsx("span",{className:"term-folder-count",children:R.length})]}),m&&n.jsx("div",{className:"term-folder-body",children:R.map(I)})]})]})]})]}),n.jsxs("div",{className:"term-main",children:[w&&n.jsx("div",{className:"drawer-backdrop",onClick:()=>f(!1)}),n.jsx("button",{type:"button",className:"term-side-toggle",title:s("commands"),onClick:()=>f(e=>!e),children:n.jsx(ne,{})}),t.terminals.length===0?n.jsxs("div",{className:"term-empty",children:[n.jsx(te,{className:"term-empty-icon"}),n.jsx("div",{className:"term-empty-title",children:s("builtinTerminal")}),n.jsx("div",{className:"term-empty-sub",children:s("termEmptySub")})]}):t.terminals.map(e=>n.jsx(ce,{conversationId:e.conversationId,terminalId:e.id,command:e.command,cwd:e.cwd,title:e.title,active:e.id===h,running:e.running,exitCode:e.exitCode,register:c.register},`${e.conversationId}:${e.id}`))]})]})}export{pe as TerminalPanel};