pi-web-ui 0.96.1 → 0.98.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +144 -2
  2. package/README.md +29 -3
  3. package/README.zh-CN.md +28 -3
  4. package/bin/pi-web-ui.mjs +75 -25
  5. package/dist/server/agent-service.js +1829 -262
  6. package/dist/server/approval-rules.js +5 -10
  7. package/dist/server/atomic-file.js +40 -0
  8. package/dist/server/attachments.js +22 -0
  9. package/dist/server/claim-store.js +5 -11
  10. package/dist/server/client-state.js +296 -11
  11. package/dist/server/compact-context-tool.js +3 -1
  12. package/dist/server/compacted-history.js +148 -0
  13. package/dist/server/composer-drafts.js +4 -6
  14. package/dist/server/control-socket.js +0 -40
  15. package/dist/server/delegate-mode.js +143 -0
  16. package/dist/server/delegate-task.js +10 -10
  17. package/dist/server/dsh/dsh-agent-service.js +66 -31
  18. package/dist/server/dsh/dsh-client.js +48 -10
  19. package/dist/server/dsh/dsh-serialize.js +12 -10
  20. package/dist/server/dsh/dsh-sessions.js +20 -6
  21. package/dist/server/dsh/runtime/goal-rpc.mjs +14 -1
  22. package/dist/server/dsh/runtime/runtime-root.mjs +1 -1
  23. package/dist/server/ensure-bash.js +3 -5
  24. package/dist/server/eval-tool.js +11 -5
  25. package/dist/server/goal-review-gate.js +250 -0
  26. package/dist/server/goal-service.js +1272 -448
  27. package/dist/server/index.js +118 -16
  28. package/dist/server/launch-origin.js +0 -4
  29. package/dist/server/locales.js +3 -5
  30. package/dist/server/lsp-tool.js +123 -24
  31. package/dist/server/markers/builtins/notify.js +4 -2
  32. package/dist/server/markers/builtins/rename.js +0 -1
  33. package/dist/server/markers/builtins/todo.js +6 -28
  34. package/dist/server/markers/marker.js +0 -15
  35. package/dist/server/markers/registry.js +0 -3
  36. package/dist/server/markers/store.js +0 -3
  37. package/dist/server/mcp-bridge.js +13 -0
  38. package/dist/server/mcp-hot-reload.js +12 -5
  39. package/dist/server/model-admin.js +19 -18
  40. package/dist/server/model-enrich.js +0 -3
  41. package/dist/server/patch-tool.js +22 -6
  42. package/dist/server/patch-turn-end-boundary.js +77 -0
  43. package/dist/server/permission-preset.js +69 -0
  44. package/dist/server/plan-manager.js +150 -5
  45. package/dist/server/plan-mode.js +414 -0
  46. package/dist/server/plugin-api-catalog.js +2 -0
  47. package/dist/server/plugin-catalog.js +6 -6
  48. package/dist/server/plugin-dom.js +3 -4
  49. package/dist/server/plugin-facilities.js +39 -8
  50. package/dist/server/plugin-grants.js +5 -8
  51. package/dist/server/plugin-permissions.js +4 -6
  52. package/dist/server/plugin-schedule.js +4 -7
  53. package/dist/server/plugin-updater.js +151 -38
  54. package/dist/server/plugins.js +22 -5
  55. package/dist/server/prompt-composer.js +12 -3
  56. package/dist/server/protocol-version.js +1 -1
  57. package/dist/server/resolve-global-sdk.js +8 -0
  58. package/dist/server/schedule-agent-tool.js +5 -1
  59. package/dist/server/scheduler-tasks.js +46 -27
  60. package/dist/server/sdk-origin.js +27 -0
  61. package/dist/server/serialize.js +33 -9
  62. package/dist/server/settings-service.js +18 -6
  63. package/dist/server/slash-commands.js +16 -0
  64. package/dist/server/subagent-templates.js +4 -10
  65. package/dist/server/subagents.js +379 -416
  66. package/dist/server/terminals.js +11 -6
  67. package/dist/server/text-quote.js +59 -0
  68. package/dist/server/text-sniff.js +15 -9
  69. package/dist/server/tool-manager.js +60 -7
  70. package/dist/server/tool-overrides.js +24 -0
  71. package/dist/server/update-check.js +80 -28
  72. package/dist/server/wait-subscription-scan.js +5 -1
  73. package/extensions/webui.ts +15 -1
  74. package/package.json +33 -21
  75. package/plugin-sdk/client-utils.mjs +92 -0
  76. package/plugin-sdk/index.d.ts +21 -0
  77. package/plugin-sdk/index.mjs +2 -0
  78. package/plugins/catalog.json +7 -0
  79. package/themes/zhupi-dark.css +5 -30
  80. package/themes/zhupi.css +5 -30
  81. package/web/dist/assets/{TerminalPanel-O3GyFIGG.js → TerminalPanel-C88r3r3Q.js} +1 -1
  82. package/web/dist/assets/index-2jDtjHvW.css +1 -0
  83. package/web/dist/assets/index-BjdK7s1M.js +368 -0
  84. package/web/dist/index.html +2 -2
  85. package/web/dist/sw.js +2 -1
  86. package/web/public/sw.js +2 -1
  87. package/web/dist/assets/index-B7Zp23Vc.css +0 -1
  88. package/web/dist/assets/index-ByfoaDwp.js +0 -366
@@ -10,9 +10,10 @@
10
10
  * 定时策略:单个 10s ticker 检查到期任务(cron 下次触发 / 间隔毫秒),
11
11
  * running 集合防重叠;catchUp "once" 在启动时补跑一次漏掉的触发。
12
12
  */
13
- import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
13
+ import { readFileSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { nextCronFire, parseCronSpec } from "./plugin-schedule.js";
16
+ import { writeJsonAtomicSync } from "./atomic-file.js";
16
17
  export const SCHEDULER_FILE = "scheduler-tasks.json";
17
18
  /** 持久间隔底线 60s(与 host.schedule 持久任务同口径,防 token 烧穿)。 */
18
19
  export const SCHEDULER_MIN_INTERVAL_MS = 60_000;
@@ -30,50 +31,72 @@ function randomTaskId() {
30
31
  s += abc[Math.floor(Math.random() * abc.length)];
31
32
  return `task-${s}`;
32
33
  }
33
- /** 归一化 + 校验用户输入;非法抛 Error(中文信息,直接面向 UI)。纯函数,可单测。 */
34
+ export class SchedulerValidationError extends Error {
35
+ code;
36
+ messageZh;
37
+ messageEn;
38
+ constructor(code, messageZh, messageEn) {
39
+ super(messageZh);
40
+ this.code = code;
41
+ this.messageZh = messageZh;
42
+ this.messageEn = messageEn;
43
+ this.name = "SchedulerValidationError";
44
+ }
45
+ }
46
+ /** 归一化 + 校验用户输入;非法抛 SchedulerValidationError。纯函数,可单测。 */
34
47
  export function normalizeSchedulerInput(input, now = Date.now()) {
35
48
  const idRaw = String(input.id ?? "").trim();
36
49
  const id = idRaw || randomTaskId();
37
- if (!SCHEDULER_ID_RE.test(id))
38
- throw new Error("任务 id 非法(只收字母/数字/下划线/连字符,≤64 字)");
50
+ if (!SCHEDULER_ID_RE.test(id)) {
51
+ throw new SchedulerValidationError("invalid_id", "任务 id 非法(只收字母/数字/下划线/连字符,≤64 字)", "Invalid task id (letters/numbers/underscores/hyphens only, <= 64 chars)");
52
+ }
39
53
  const name = String(input.name ?? "")
40
54
  .trim()
41
55
  .slice(0, 80);
42
- if (!name)
43
- throw new Error("任务名称不能为空");
56
+ if (!name) {
57
+ throw new SchedulerValidationError("empty_name", "任务名称不能为空", "Task name cannot be empty");
58
+ }
44
59
  const description = String(input.description ?? "")
45
60
  .trim()
46
61
  .slice(0, 500);
47
62
  const cwd = String(input.cwd ?? "").trim();
48
- if (!cwd)
49
- throw new Error("执行目标项目(cwd)不能为空");
63
+ if (!cwd) {
64
+ throw new SchedulerValidationError("empty_cwd", "执行目标项目(cwd)不能为空", "Target project directory (cwd) cannot be empty");
65
+ }
50
66
  const kind = input.kind === "interval" ? "interval" : "cron";
51
67
  let spec;
52
68
  if (kind === "cron") {
53
69
  spec = String(input.spec ?? "")
54
70
  .trim()
55
71
  .replace(/\s+/g, " ");
56
- if (!parseCronSpec(spec))
57
- throw new Error("cron 表达式非法(要 5 字段:分 时 日 月 周,如 0 9 * * *)");
72
+ if (!parseCronSpec(spec)) {
73
+ throw new SchedulerValidationError("invalid_cron", "cron 表达式非法(要 5 字段:分 时 日 月 周,如 0 9 * * *)", "Invalid cron expression (requires 5 fields: min hour day month weekday, e.g. 0 9 * * *)");
74
+ }
58
75
  }
59
76
  else {
60
77
  const ms = Math.floor(Number(input.spec));
61
- if (!Number.isFinite(ms) || ms <= 0)
62
- throw new Error("间隔毫秒数非法");
63
- if (ms < SCHEDULER_MIN_INTERVAL_MS)
64
- throw new Error(`间隔太短(最短 ${SCHEDULER_MIN_INTERVAL_MS / 1000}s,防 token 烧穿)`);
65
- if (ms > SCHEDULER_MAX_INTERVAL_MS)
66
- throw new Error("间隔太长(最长 30 天)");
78
+ if (!Number.isFinite(ms) || ms <= 0) {
79
+ throw new SchedulerValidationError("invalid_interval", "间隔毫秒数非法", "Invalid interval milliseconds");
80
+ }
81
+ if (ms < SCHEDULER_MIN_INTERVAL_MS) {
82
+ throw new SchedulerValidationError("interval_too_short", `间隔太短(最短 ${SCHEDULER_MIN_INTERVAL_MS / 1000}s,防 token 烧穿)`, `Interval too short (minimum ${SCHEDULER_MIN_INTERVAL_MS / 1000}s)`);
83
+ }
84
+ if (ms > SCHEDULER_MAX_INTERVAL_MS) {
85
+ throw new SchedulerValidationError("interval_too_long", "间隔太长(最长 30 天)", "Interval too long (maximum 30 days)");
86
+ }
67
87
  spec = String(ms);
68
88
  }
69
89
  const prompt = String(input.prompt ?? "").trim();
70
- if (!prompt)
71
- throw new Error("触发指令(prompt)不能为空");
72
- if (prompt.length > 8000)
73
- throw new Error("触发指令超长(>8000 字),请裁剪后重试");
90
+ if (!prompt) {
91
+ throw new SchedulerValidationError("empty_prompt", "触发指令(prompt)不能为空", "Trigger instruction (prompt) cannot be empty");
92
+ }
93
+ if (prompt.length > 8000) {
94
+ throw new SchedulerValidationError("prompt_too_long", "触发指令超长(>8000 字),请裁剪后重试", "Trigger instruction too long (>8000 characters), please shorten and retry");
95
+ }
74
96
  const model = String(input.model ?? "").trim();
75
- if (model && !model.includes("/"))
76
- throw new Error("模型格式非法(应为 provider/id)");
97
+ if (model && !model.includes("/")) {
98
+ throw new SchedulerValidationError("invalid_model", "模型格式非法(应为 provider/id)", "Invalid model format (expected provider/id)");
99
+ }
77
100
  const thinkingLevel = String(input.thinkingLevel ?? "").trim();
78
101
  const catchUp = input.catchUp === "once" ? "once" : "skip";
79
102
  const conversationId = String(input.conversationId ?? "")
@@ -267,14 +290,10 @@ export class SchedulerStore {
267
290
  }
268
291
  save() {
269
292
  try {
270
- mkdirSync(this.dataDir, { recursive: true });
271
- const file = schedulerFile(this.dataDir);
272
- const tmp = `${file}.tmp-${process.pid}`;
273
293
  const tasks = {};
274
294
  for (const [id, t] of this.tasks)
275
295
  tasks[id] = t;
276
- writeFileSync(tmp, JSON.stringify({ v: 1, tasks }));
277
- renameSync(tmp, file);
296
+ writeJsonAtomicSync(schedulerFile(this.dataDir), { v: 1, tasks });
278
297
  }
279
298
  catch (err) {
280
299
  console.error("[scheduler] persist failed:", err);
@@ -37,6 +37,33 @@ export function sdkCopies(fromFile = import.meta.url) {
37
37
  break;
38
38
  dir = parent;
39
39
  }
40
+ // 显式透传的宿主 SDK 路径(如 extensions/webui.ts 宿主传递,issue #482)
41
+ if (process.env.PI_WEB_SDK_DIR) {
42
+ const pj = join(process.env.PI_WEB_SDK_DIR, "package.json");
43
+ if (existsSync(pj) && !out.some((c) => c.path === pj)) {
44
+ try {
45
+ const version = JSON.parse(readFileSync(pj, "utf8")).version;
46
+ if (typeof version === "string" && version) {
47
+ out.push({ path: pj, version });
48
+ }
49
+ }
50
+ catch { }
51
+ }
52
+ }
53
+ // 宿主 Pi Node 路径探测(~/.local/share/pi-node/current/...,issue #482)
54
+ const home = process.env.HOME || process.env.USERPROFILE;
55
+ if (home) {
56
+ const hostPj = join(home, ".local", "share", "pi-node", "current", "lib", "node_modules", PKG, "package.json");
57
+ if (existsSync(hostPj) && !out.some((c) => c.path === hostPj)) {
58
+ try {
59
+ const version = JSON.parse(readFileSync(hostPj, "utf8")).version;
60
+ if (typeof version === "string" && version) {
61
+ out.push({ path: hostPj, version });
62
+ }
63
+ }
64
+ catch { }
65
+ }
66
+ }
40
67
  return out;
41
68
  }
42
69
  /** 点分版本号比较(够用即可:只比数字段,缺位当 0)。 */
@@ -1,6 +1,7 @@
1
- const TEXT_CAP = 200_000;
2
- const TOOL_OUTPUT_CAP = 100_000;
3
- const ARGS_CAP = 20_000;
1
+ import { splitQuotedPrompt } from "./text-quote.js";
2
+ export const TEXT_CAP = 200_000;
3
+ export const TOOL_OUTPUT_CAP = 100_000;
4
+ export const ARGS_CAP = 20_000;
4
5
  /**
5
6
  * toolResult 里单张图片进快照的上限(dataUrl 字符数 ≈ 1.5MB 二进制)。
6
7
  * 视口/元素截图随便过;超大整页截图回落成占位文本(模型侧不受影响 —— 图它
@@ -17,10 +18,11 @@ const TOOL_RESULT_IMAGE_MAX = 8;
17
18
  * 前端还得写容错);工具作者应自己封顶(见 present-files-tool.ts 的摘录预算)。
18
19
  */
19
20
  const TOOL_DETAILS_CAP = 64_000;
20
- function truncate(s, cap) {
21
- if (s.length <= cap)
22
- return { text: s, truncated: false };
23
- return { text: `${s.slice(0, cap)}\n\n… [truncated]`, truncated: true };
21
+ export function truncate(s, cap) {
22
+ const str = typeof s === "string" ? s : String(s ?? "");
23
+ if (str.length <= cap)
24
+ return { text: str, truncated: false };
25
+ return { text: `${str.slice(0, cap)}\n\n… [truncated]`, truncated: true };
24
26
  }
25
27
  /**
26
28
  * SDK/工具的图片块 → 前端可直接 <img> 的 UiImageBlock。
@@ -177,6 +179,17 @@ export function findEntryByUiId(entries, messageId, seqOf) {
177
179
  if (uiMessageId(m, seqOf(m)) === messageId)
178
180
  return entry;
179
181
  }
182
+ else if (entry.type === "compaction") {
183
+ const ts = entry.timestamp ? new Date(entry.timestamp).getTime() : 0;
184
+ const m = {
185
+ role: "compactionSummary",
186
+ summary: entry.summary ?? "",
187
+ tokensBefore: entry.tokensBefore,
188
+ timestamp: ts,
189
+ };
190
+ if (uiMessageId(m, seqOf(m)) === messageId)
191
+ return entry;
192
+ }
180
193
  }
181
194
  return null;
182
195
  }
@@ -193,13 +206,24 @@ export function serializeMessage(m, seq) {
193
206
  }
194
207
  const id = uiMessageId(m, seq);
195
208
  switch (m.role) {
196
- case "user":
209
+ case "user": {
210
+ const quotes = [];
211
+ const raw = typeof m.content === "string" ? [{ type: "text", text: m.content }] : m.content;
212
+ const content = raw.map((block) => {
213
+ if (block.type !== "text")
214
+ return block;
215
+ const parsed = splitQuotedPrompt(block.text);
216
+ quotes.push(...parsed.quotes);
217
+ return { ...block, text: parsed.text };
218
+ });
197
219
  return {
198
220
  id,
199
221
  role: "user",
200
- content: serializeUserContent(m.content),
222
+ content: serializeUserContent(content),
223
+ ...(quotes.length ? { details: { quotes } } : {}),
201
224
  timestamp: m.timestamp,
202
225
  };
226
+ }
203
227
  case "assistant":
204
228
  return {
205
229
  id,
@@ -13,6 +13,7 @@ import { extensionKey, normalizeDisabledPluginTools, normalizeRetryMaxAttempts,
13
13
  import { normalizeSoftCapByModel, normalizeSoftCapTokens } from "./soft-cap.js";
14
14
  import { findVisionModels, SYSTEM_PROMPT } from "./vision-bridge.js";
15
15
  import { COMMITMSG_SYSTEM_PROMPT } from "./scm-commitmsg.js";
16
+ import { PLAN_MODE_SYSTEM_PROMPT } from "./plan-mode.js";
16
17
  import { DEFAULT_TEMPLATES } from "./subagent-templates.js";
17
18
  import { deriveLegacy, foldLegacyIntoDisabled, normalizeDisabledAgentTools } from "./tool-manager.js";
18
19
  export class SettingsService {
@@ -54,12 +55,6 @@ export class SettingsService {
54
55
  }
55
56
  return false;
56
57
  }
57
- get reviewPrefs() {
58
- return {
59
- reviewPrompt: this.settings.reviewPrompt,
60
- reviewDisabledSkills: this.settings.reviewDisabledSkills,
61
- };
62
- }
63
58
  hasPendingReload() {
64
59
  return this.pendingReload;
65
60
  }
@@ -314,6 +309,8 @@ export class SettingsService {
314
309
  visionBridgePrompt: this.settings.visionBridgePrompt,
315
310
  scmCommitMsgPromptMode: this.settings.scmCommitMsgPromptMode,
316
311
  scmCommitMsgPrompt: this.settings.scmCommitMsgPrompt,
312
+ planModePromptMode: this.settings.planModePromptMode,
313
+ planModePrompt: this.settings.planModePrompt,
317
314
  reviewPrompt: this.settings.reviewPrompt,
318
315
  reviewDisabledSkills: [...this.settings.reviewDisabledSkills],
319
316
  disabledPlugins: [...(this.settings.disabledPlugins ?? [])],
@@ -327,6 +324,7 @@ export class SettingsService {
327
324
  toolsSchema: promptSnap.toolsSchema,
328
325
  visionBridgeDefaultPrompt: SYSTEM_PROMPT,
329
326
  scmCommitMsgDefaultPrompt: COMMITMSG_SYSTEM_PROMPT,
327
+ planModeDefaultPrompt: PLAN_MODE_SYSTEM_PROMPT,
330
328
  visionModels: this.collectVisionModels(),
331
329
  disabledSkills: [...this.settings.disabledSkills],
332
330
  disabledExtensions: [...this.settings.disabledExtensions],
@@ -404,6 +402,7 @@ export class SettingsService {
404
402
  // 统一工具开关 live 生效(ActiveSet 加减,无需 reload),见末尾 applyToolGating。
405
403
  const toolGatingChanged = partial.disabledAgentTools !== undefined ||
406
404
  partial.disabledPluginTools !== undefined ||
405
+ partial.disabledPlugins !== undefined ||
407
406
  partial.terminalToolsEnabled !== undefined ||
408
407
  partial.editSoftEnabled !== undefined ||
409
408
  partial.questionnaireEnabled !== undefined;
@@ -484,8 +483,12 @@ export class SettingsService {
484
483
  this.settings.toolApprovalEnabled = partial.toolApprovalEnabled;
485
484
  }
486
485
  // 目标模式总开关:运行时无需重载(goal bar / 服务端入口实时读取)。
486
+ // on→off 时立即停掉在飞的目标/调研 —— 不停的话循环照跑照派,开关名存实亡。
487
487
  if (partial.goalModeEnabled !== undefined) {
488
+ const wasOn = this.settings.goalModeEnabled !== false;
488
489
  this.settings.goalModeEnabled = partial.goalModeEnabled;
490
+ if (wasOn && partial.goalModeEnabled === false)
491
+ this.host.onGoalModeDisabled?.();
489
492
  }
490
493
  // 同项目并行提醒开关:同上,发送入口逐轮实时读取,无需 reload。
491
494
  if (partial.parallelReminderEnabled !== undefined) {
@@ -529,6 +532,12 @@ export class SettingsService {
529
532
  if (partial.scmCommitMsgPrompt !== undefined) {
530
533
  this.settings.scmCommitMsgPrompt = partial.scmCommitMsgPrompt;
531
534
  }
535
+ if (partial.planModePromptMode !== undefined) {
536
+ this.settings.planModePromptMode = partial.planModePromptMode;
537
+ }
538
+ if (partial.planModePrompt !== undefined) {
539
+ this.settings.planModePrompt = partial.planModePrompt;
540
+ }
532
541
  if (partial.reviewPrompt !== undefined) {
533
542
  this.settings.reviewPrompt = partial.reviewPrompt;
534
543
  }
@@ -693,6 +702,9 @@ export class SettingsService {
693
702
  // 「AI 提交信息」提示词同样不进预设——保留当前值。
694
703
  scmCommitMsgPromptMode: this.settings.scmCommitMsgPromptMode,
695
704
  scmCommitMsgPrompt: this.settings.scmCommitMsgPrompt,
705
+ // 计划模式提示词同样不进预设——保留当前值。
706
+ planModePromptMode: this.settings.planModePromptMode,
707
+ planModePrompt: this.settings.planModePrompt,
696
708
  // 子代理默认模型也不进预设——保留当前值。
697
709
  subagentDefaultModel: this.settings.subagentDefaultModel,
698
710
  // 快捷短语是纯 UI 偏好,不进预设——保留当前值。
@@ -38,6 +38,13 @@ export const NATIVE_COMMANDS = [
38
38
  argumentHint: "<路径>",
39
39
  argumentHintEn: "<path>",
40
40
  },
41
+ {
42
+ name: "plan",
43
+ description: "开启或关闭计划模式(/plan [on|off])",
44
+ descriptionEn: "Toggle plan mode (/plan [on|off])",
45
+ argumentHint: "[on|off]",
46
+ argumentHintEn: "[on|off]",
47
+ },
41
48
  {
42
49
  name: "thinking",
43
50
  description: "设置思考强度",
@@ -249,6 +256,15 @@ export class SlashCommandsService {
249
256
  await this.host.setCwd(args);
250
257
  }
251
258
  return true;
259
+ case "plan": {
260
+ const trimmed = args.trim().toLowerCase();
261
+ const current = this.host.getPlanMode ? this.host.getPlanMode() : false;
262
+ const next = trimmed === "on" ? true : trimmed === "off" ? false : !current;
263
+ if (this.host.setPlanMode) {
264
+ await this.host.setPlanMode(next);
265
+ }
266
+ return true;
267
+ }
252
268
  case "thinking": {
253
269
  const ALIAS = {
254
270
  off: "off",
@@ -19,8 +19,8 @@
19
19
  * systemPromptEn 为英文(en 用,缺失回落中文);调用方用 pickTemplateDescription/
20
20
  * pickTemplatePrompt 按语言选用。
21
21
  */
22
- import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
23
- import { dirname } from "node:path";
22
+ import { readFileSync } from "node:fs";
23
+ import { writeJsonAtomicSync } from "./atomic-file.js";
24
24
  /**
25
25
  * 思考强度取值:与 SDK 的 THINKING_LEVEL_OPTIONS(前端 ModelThinking 的 THINKING_VALUES)
26
26
  * 一致。SDK 未从包根导出该常量,这里照抄一份作输入校验(写错的值一律当未配置)。
@@ -608,10 +608,7 @@ export class SubagentTemplatesStore {
608
608
  }
609
609
  persist() {
610
610
  try {
611
- mkdirSync(dirname(this.filePath), { recursive: true });
612
- const tmp = `${this.filePath}.${process.pid}.tmp`;
613
- writeFileSync(tmp, JSON.stringify(this.templates, null, 2) + "\n");
614
- renameSync(tmp, this.filePath);
611
+ writeJsonAtomicSync(this.filePath, this.templates);
615
612
  }
616
613
  catch {
617
614
  // best effort
@@ -644,10 +641,7 @@ export class SubagentTemplatesStore {
644
641
  saveSeeded(names) {
645
642
  this.seededNames = names;
646
643
  try {
647
- mkdirSync(dirname(this.seededPath()), { recursive: true });
648
- const tmp = `${this.seededPath()}.tmp`;
649
- writeFileSync(tmp, JSON.stringify([...names].sort(), null, 2));
650
- renameSync(tmp, this.seededPath());
644
+ writeJsonAtomicSync(this.seededPath(), [...names].sort());
651
645
  }
652
646
  catch {
653
647
  // best effort