pi-web-ui 0.68.1 → 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.
Files changed (50) hide show
  1. package/dist/server/agent-service.js +306 -102
  2. package/dist/server/attachments.js +7 -2
  3. package/dist/server/client-state.js +31 -2
  4. package/dist/server/dsh/dsh-agent-service.js +154 -39
  5. package/dist/server/dsh/dsh-client.js +9 -8
  6. package/dist/server/dsh/dsh-sessions.js +4 -3
  7. package/dist/server/dsh/runtime/runtime-root.mjs +17 -11
  8. package/dist/server/edit-soft-tool.js +33 -26
  9. package/dist/server/files-service.js +12 -7
  10. package/dist/server/goal-service.js +85 -24
  11. package/dist/server/i18n.js +157 -0
  12. package/dist/server/index.js +124 -6
  13. package/dist/server/locales.js +210 -0
  14. package/dist/server/managed.js +61 -0
  15. package/dist/server/marker-service.js +20 -7
  16. package/dist/server/markers/builtins/notify.js +19 -6
  17. package/dist/server/markers/builtins/rename.js +41 -8
  18. package/dist/server/markers/builtins/todo.js +107 -30
  19. package/dist/server/markers/registry.js +2 -2
  20. package/dist/server/mcp-bridge.js +3 -1
  21. package/dist/server/model-admin.js +25 -14
  22. package/dist/server/plugin-catalog.js +7 -3
  23. package/dist/server/plugin-updater.js +6 -2
  24. package/dist/server/plugins.js +40 -17
  25. package/dist/server/prompt-composer.js +76 -16
  26. package/dist/server/protocol-version.js +1 -1
  27. package/dist/server/scm.js +18 -25
  28. package/dist/server/serialize.js +1 -0
  29. package/dist/server/settings-service.js +29 -1
  30. package/dist/server/subagent-templates.js +105 -0
  31. package/dist/server/subagents.js +155 -54
  32. package/dist/server/tabs.js +87 -0
  33. package/dist/server/terminals.js +88 -48
  34. package/dist/server/update-check.js +56 -52
  35. package/dist/server/vision-bridge.js +34 -12
  36. package/package.json +4 -1
  37. package/web/dist/assets/TerminalPanel-Cj8zsjx-.js +6 -0
  38. package/web/dist/assets/TerminalPanel-DOrYoP_4.css +32 -0
  39. package/web/dist/assets/index-DCOcsPFm.js +334 -0
  40. package/web/dist/assets/index-jH2Bb-0X.css +10 -0
  41. package/web/dist/assets/markdown-Cpo0pNcR.js +51 -0
  42. package/web/dist/assets/{react-C9ovnpIm.js → react-CtudoG1_.js} +2 -2
  43. package/web/dist/assets/xterm-B96xOxS9.js +38 -0
  44. package/web/dist/index.html +4 -4
  45. package/web/dist/assets/TerminalPanel-6GBZ9nXN.css +0 -32
  46. package/web/dist/assets/TerminalPanel-IJF_fssI.js +0 -6
  47. package/web/dist/assets/index-BmiyyjKp.css +0 -10
  48. package/web/dist/assets/index-qoTr5KXy.js +0 -332
  49. package/web/dist/assets/markdown-DRBrS2Nf.js +0 -51
  50. package/web/dist/assets/xterm-D1D2FVe3.js +0 -38
@@ -17,6 +17,7 @@
17
17
  * 本模块是纯函数(不 import SDK / node),浏览器端可复用(SettingsModal 需要
18
18
  * DEFAULT_PROMPT_TEMPLATE 与 token 元数据)。
19
19
  */
20
+ import { getServerBlock, pick } from "./i18n.js";
20
21
  /** 全部来源 token。默认模板顺序即此数组顺序。 */
21
22
  export const PROMPT_TOKENS = [
22
23
  "soul", // 内置灵魂提示词(persona;有 SYSTEM.md 时其内容)
@@ -31,6 +32,25 @@ export const PROMPT_TOKENS = [
31
32
  "skills", // 技能段 <available_skills>
32
33
  "cwd", // Current working directory 行
33
34
  ];
35
+ /** 只读来源(设置面板只读展示、不提供覆盖输入):这些 token 的内容由系统/环境
36
+ * 在每次 run 时动态生成(工具集、项目文件、技能、工作目录、开关状态),用户无法
37
+ * 在设置里预设其内容。反之 soul / guidelines / append 是用户内容层,可编辑。
38
+ * 注意:只读是「设置面板 UI」层面;服务端 override 机制(renderPromptTemplate)
39
+ * 仍保留,以兼容旧配置与编程调用。 */
40
+ export const READONLY_PROMPT_SOURCES = [
41
+ "tools", // 工具列表(运行时时按已注册工具动态生成)
42
+ "pi_docs", // Pi 文档指引(指向已安装 pi 包路径,由安装位置决定)
43
+ "persona", // Windows persona(仅 win32,平台固定)
44
+ "terminal", // 终端工具使用引导(随「终端工具」开关)
45
+ "markers", // 内置标记工具引导(随 markers 开关)
46
+ "context", // 项目上下文(AGENTS.md / CLAUDE.md 收集结果)
47
+ "skills", // 技能段(来自环境/技能文件)
48
+ "cwd", // 当前工作目录
49
+ ];
50
+ /** 仅当 token 是只读来源时返回 true(设置面板用于判断是否展示覆盖输入框)。 */
51
+ export function isReadonlyPromptSource(token) {
52
+ return READONLY_PROMPT_SOURCES.includes(token);
53
+ }
34
54
  /** 默认模板:全部 token 按自然顺序以空行连接 —— 无覆盖、不改动时渲染结果 ≈
35
55
  * SDK 默认拼装的完整提示词。 */
36
56
  export const DEFAULT_PROMPT_TEMPLATE = PROMPT_TOKENS.map((t) => `{{${t}}}`).join("\n\n");
@@ -59,8 +79,25 @@ export function overrideOf(overrides, token) {
59
79
  }
60
80
  /** 内置默认灵魂段落(buildSystemPrompt 默认分支的开头,与 SDK 同步维护)。 */
61
81
  export const BUILTIN_SOUL = "You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
82
+ /**
83
+ * 内置默认灵魂段落的中文版(issue #91):lang === "zh" 且调用方未自定义
84
+ * builtinSoul 时 {{soul}} 用它。英文版 BUILTIN_SOUL 保持原样(英文默认)。
85
+ */
86
+ export const BUILTIN_SOUL_ZH = "你是运行在 pi(一个编码智能体框架)中的专业编码助手。你通过读取文件、执行命令、编辑代码和新建文件来帮助用户。";
62
87
  /** Pi documentation 段模板 —— 与 SDK buildSystemPrompt 默认分支一致(路径由调用方注入)。 */
63
- export function buildPiDocsText(readme, docs, examples) {
88
+ export function buildPiDocsText(readme, docs, examples, lang = "en") {
89
+ if (lang === "zh") {
90
+ return [
91
+ "Pi 文档(仅当用户问及 pi 本身、其 SDK、扩展、主题、技能或 TUI 时阅读):",
92
+ `- 主文档:${readme}`,
93
+ `- 更多文档:${docs}`,
94
+ `- 示例:${examples}(扩展、自定义工具、SDK)`,
95
+ "- 阅读 pi 文档或示例时,在「更多文档」下找 docs/...、在「示例」下找 examples/...,不要按当前工作目录解析",
96
+ "- 被问及以下主题时:扩展(docs/extensions.md、examples/extensions/)、主题(docs/themes.md)、技能(docs/skills.md)、提示词模板(docs/prompt-templates.md)、TUI 组件(docs/tui.md)、快捷键(docs/keybindings.md)、SDK 集成(docs/sdk.md)、自定义服务商(docs/custom-provider.md)、添加模型(docs/models.md)、pi 包(docs/packages.md)、环境变量(docs/environment-variables.md)",
97
+ "- 处理 pi 相关主题时,先阅读文档和示例,并跟随其中的 .md 交叉引用,再动手实现",
98
+ "- pi 的 .md 文件务必通读全文,并跟随其中指向相关文档的链接(例如 TUI API 细节见 tui.md)",
99
+ ].join("\n");
100
+ }
64
101
  return [
65
102
  "Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):",
66
103
  `- Main documentation: ${readme}`,
@@ -75,6 +112,7 @@ export function buildPiDocsText(readme, docs, examples) {
75
112
  /** Guidelines 段:文件探索引导 + 工具 promptGuidelines(去重)+ 固定两行。
76
113
  * 与 buildSystemPrompt 默认分支的聚合规则一致。 */
77
114
  function buildGuidelinesText(inputs) {
115
+ const lang = inputs.lang ?? "en";
78
116
  const selected = new Set(inputs.selectedTools);
79
117
  const lines = [];
80
118
  const add = (g) => {
@@ -85,14 +123,14 @@ function buildGuidelinesText(inputs) {
85
123
  const has = (n) => selected.has(n);
86
124
  if ((has("bash") || has("powershell")) && !has("grep") && !has("find") && !has("ls")) {
87
125
  add(has("bash") && has("powershell")
88
- ? "Use bash or PowerShell for file operations like listing, searching, and finding files"
89
- : "Use bash for file operations like ls, rg, find");
126
+ ? pick(lang, "涉及列出、搜索、查找文件等文件操作时,使用 bash 或 PowerShell", "Use bash or PowerShell for file operations like listing, searching, and finding files", "prompt.guidelines.bash.powershell")
127
+ : pick(lang, "涉及 ls、rg、find 等文件操作时,使用 bash", "Use bash for file operations like ls, rg, find", "prompt.guidelines.bash.basic"));
90
128
  }
91
129
  for (const g of inputs.toolGuidelines)
92
130
  add(g);
93
- add("Be concise in your responses");
94
- add("Show file paths clearly when working with files");
95
- return `Guidelines:\n${lines.map((l) => `- ${l}`).join("\n")}`;
131
+ add(pick(lang, "回答要简洁", "Be concise in your responses", "prompt.guidelines.be.concise"));
132
+ add(pick(lang, "处理文件时清楚地给出文件路径", "Show file paths clearly when working with files", "prompt.guidelines.show.paths"));
133
+ return `${pick(lang, "指导原则:", "Guidelines:", "prompt.guidelines.title")}\n${lines.map((l) => `- ${l}`).join("\n")}`;
96
134
  }
97
135
  function escapeXml(s) {
98
136
  return s
@@ -103,14 +141,14 @@ function escapeXml(s) {
103
141
  .replace(/'/g, "&apos;");
104
142
  }
105
143
  /** 技能段文本(不含前导空行)。与 SDK formatSkillsForPrompt 一致。 */
106
- export function buildSkillsText(skills) {
144
+ export function buildSkillsText(skills, lang = "en") {
107
145
  const visible = skills.filter((s) => !s.disableModelInvocation);
108
146
  if (visible.length === 0)
109
147
  return "";
110
148
  const lines = [
111
- "The following skills provide specialized instructions for specific tasks.",
112
- "Use the read tool to load a skill's file when the task matches its description.",
113
- "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
149
+ pick(lang, "以下技能为特定任务提供专门的指令。", "The following skills provide specialized instructions for specific tasks.", "prompt.skills.intro.specialized"),
150
+ pick(lang, "当任务与某技能的描述相符时,用 read 工具加载该技能文件。", "Use the read tool to load a skill's file when the task matches its description.", "prompt.skills.intro.use.read"),
151
+ pick(lang, "当技能文件引用相对路径时,以技能目录(SKILL.md 的父目录 / 该路径的 dirname)为基准解析,并在工具命令中使用解析后的绝对路径。", "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.", "prompt.skills.intro.resolve.path"),
114
152
  "",
115
153
  "<available_skills>",
116
154
  ];
@@ -125,13 +163,13 @@ export function buildSkillsText(skills) {
125
163
  return lines.join("\n");
126
164
  }
127
165
  /** 项目上下文块(不含前导空行)。 */
128
- function buildContextText(files) {
166
+ function buildContextText(files, lang = "en") {
129
167
  if (files.length === 0)
130
168
  return "";
131
169
  return [
132
170
  "<project_context>",
133
171
  "",
134
- "Project-specific instructions and guidelines:",
172
+ pick(lang, "项目专属指令与规范:", "Project-specific instructions and guidelines:", "prompt.context.title"),
135
173
  "",
136
174
  ...files.map((f) => `<project_instructions path="${f.path}">\n${f.content}\n</project_instructions>`),
137
175
  "",
@@ -147,20 +185,42 @@ function buildToolsText(inputs) {
147
185
  "In addition to the tools above, you may have access to other custom tools depending on the project.",
148
186
  ].join("\n\n");
149
187
  }
188
+ /** 工具 schema 只读文本(设置面板「查看当前完整提示词」里展示发给模型的完整
189
+ * 工具定义:name + description + parameters JSON Schema)。 */
190
+ export function buildToolsSchemaText(tools) {
191
+ if (tools.length === 0)
192
+ return "";
193
+ return tools
194
+ .map((t) => {
195
+ const lines = [`## ${t.name}`];
196
+ if (t.description && t.description.trim())
197
+ lines.push(t.description.trim());
198
+ lines.push("", "Parameters (JSON Schema):", JSON.stringify(t.parameters ?? {}, null, 2));
199
+ return lines.join("\n");
200
+ })
201
+ .join("\n\n");
202
+ }
150
203
  /** 计算每个 token 的自动内容(无覆盖时的展开值)。 */
151
204
  export function resolveSectionTexts(inputs) {
152
205
  const cwd = inputs.cwd.replace(/\\/g, "/");
206
+ const lang = inputs.lang ?? "en";
207
+ // soul:用户 SYSTEM.md 优先;无则按 lang 选内置默认(调用方自定义的 builtinSoul 原样保留)。
208
+ let soul = inputs.systemPromptFile?.trim() ? inputs.systemPromptFile : inputs.builtinSoul;
209
+ if (lang === "zh" && soul === BUILTIN_SOUL)
210
+ soul = BUILTIN_SOUL_ZH;
211
+ else if (soul === BUILTIN_SOUL)
212
+ soul = getServerBlock(lang, "prompt.soul", BUILTIN_SOUL_ZH.split("\n"), BUILTIN_SOUL.split("\n")).join("\n");
153
213
  return {
154
- soul: inputs.systemPromptFile?.trim() ? inputs.systemPromptFile : inputs.builtinSoul,
214
+ soul,
155
215
  tools: buildToolsText(inputs),
156
216
  guidelines: buildGuidelinesText(inputs),
157
- pi_docs: buildPiDocsText(inputs.piReadme, inputs.piDocs, inputs.piExamples),
217
+ pi_docs: buildPiDocsText(inputs.piReadme, inputs.piDocs, inputs.piExamples, lang),
158
218
  append: inputs.appendFiles.join("\n\n"),
159
219
  persona: inputs.windowsPersona,
160
220
  terminal: inputs.terminalGuidance,
161
221
  markers: inputs.markersGuidance,
162
- context: buildContextText(inputs.contextFiles),
163
- skills: buildSkillsText(inputs.skills),
222
+ context: buildContextText(inputs.contextFiles, lang),
223
+ skills: buildSkillsText(inputs.skills, lang),
164
224
  cwd: `Current working directory: ${cwd}`,
165
225
  };
166
226
  }
@@ -8,4 +8,4 @@
8
8
  * its own copy in web/src/protocol-version.ts; scripts/check-protocol-sync.mjs
9
9
  * verifies the two never drift.
10
10
  */
11
- export const PROTOCOL_VERSION = 12;
11
+ export const PROTOCOL_VERSION = 14;
@@ -8,12 +8,14 @@
8
8
  */
9
9
  import { execFile } from "node:child_process";
10
10
  import { promisify } from "node:util";
11
+ import { pick } from "./i18n.js";
11
12
  const exec = promisify(execFile);
12
13
  /** Per-command timeout / output cap — a stuck repo must not hang the panel. */
13
14
  const GIT_TIMEOUT_MS = 15_000;
14
15
  const MAX_GIT_OUTPUT = 16 * 1024 * 1024;
15
16
  /** Run one git command; throws Error with a readable message on failure. */
16
- async function git(cwd, args) {
17
+ async function git(cwd, args, lang) {
18
+ const l = lang?.() ?? "en";
17
19
  try {
18
20
  const { stdout } = await exec("git", ["-c", "core.quotepath=false", ...args], {
19
21
  cwd,
@@ -26,11 +28,11 @@ async function git(cwd, args) {
26
28
  catch (err) {
27
29
  const e = err;
28
30
  if (e.code === "ENOENT")
29
- throw new Error("未找到 git 命令——请确认已安装 Git 并在 PATH 中");
31
+ throw new Error(pick(l, "未找到 git 命令——请确认已安装 Git 并在 PATH 中", "git command not found — make sure Git is installed and on PATH", "scm.git.not.found"));
30
32
  if (e.killed)
31
- throw new Error("git 命令超时");
33
+ throw new Error(pick(l, "git 命令超时", "git command timed out", "scm.git.timeout"));
32
34
  const detail = (e.stderr ?? e.message ?? "").trim().split("\n")[0];
33
- throw new Error(detail || "git 命令失败");
35
+ throw new Error(detail || pick(l, "git 命令失败", "git command failed", "scm.git.failed"));
34
36
  }
35
37
  }
36
38
  /** Absolute path of the repo's git dir (handles worktrees/submodules), or
@@ -244,16 +246,16 @@ const HISTORY_ARGS = [
244
246
  ];
245
247
  /** Commit graph for the history tab — fetched lazily so the common
246
248
  * "changes" view never pays for it on huge repos. */
247
- export async function scmHistory(cwd) {
248
- return parseCommitHistory(await git(cwd, HISTORY_ARGS));
249
+ export async function scmHistory(cwd, lang) {
250
+ return parseCommitHistory(await git(cwd, HISTORY_ARGS, lang));
249
251
  }
250
252
  /** Status refresh payload (status + branches + numstat) — parallel. */
251
- export async function scmStatus(cwd) {
253
+ export async function scmStatus(cwd, lang) {
252
254
  const [statusText, branchText, statText, cachedStatText] = await Promise.all([
253
- git(cwd, ["status", "--porcelain=v1", "-b", "--find-renames"]),
254
- git(cwd, ["for-each-ref", "refs/heads", "refs/remotes", "--format=%(refname)%09%(HEAD)"]),
255
- git(cwd, ["diff", "--numstat"]),
256
- git(cwd, ["diff", "--cached", "--numstat"]),
255
+ git(cwd, ["status", "--porcelain=v1", "-b", "--find-renames"], lang),
256
+ git(cwd, ["for-each-ref", "refs/heads", "refs/remotes", "--format=%(refname)%09%(HEAD)"], lang),
257
+ git(cwd, ["diff", "--numstat"], lang),
258
+ git(cwd, ["diff", "--cached", "--numstat"], lang),
257
259
  ]);
258
260
  const header = parseStatusHeader(statusText
259
261
  .split("\n")
@@ -282,23 +284,14 @@ export async function scmStatus(cwd) {
282
284
  };
283
285
  }
284
286
  /** Staged + worktree diffs for one file (empty strings when no diff). */
285
- export async function scmFileDiff(cwd, path) {
287
+ export async function scmFileDiff(cwd, path, lang) {
286
288
  const [staged, worktree] = await Promise.all([
287
- git(cwd, ["diff", "--cached", "--no-color", "--no-ext-diff", "--", path]).catch(() => ""),
288
- git(cwd, ["diff", "--no-color", "--no-ext-diff", "--", path]).catch(() => ""),
289
+ git(cwd, ["diff", "--cached", "--no-color", "--no-ext-diff", "--", path], lang).catch(() => ""),
290
+ git(cwd, ["diff", "--no-color", "--no-ext-diff", "--", path], lang).catch(() => ""),
289
291
  ]);
290
292
  return { staged, worktree };
291
293
  }
292
294
  /** Full patch of one commit (`git show`). */
293
- export async function scmCommitDetail(cwd, hash) {
294
- return git(cwd, [
295
- "show",
296
- "--no-color",
297
- "--no-ext-diff",
298
- "--find-renames",
299
- "--format=fuller",
300
- "--stat",
301
- "--patch",
302
- hash,
303
- ]);
295
+ export async function scmCommitDetail(cwd, hash, lang) {
296
+ return git(cwd, ["show", "--no-color", "--no-ext-diff", "--find-renames", "--format=fuller", "--stat", "--patch", hash], lang);
304
297
  }
@@ -166,6 +166,7 @@ export function serializeMessage(m, seq) {
166
166
  role: "compactionSummary",
167
167
  content: [{ type: "text", text, truncated }],
168
168
  timestamp: m.timestamp,
169
+ tokensBefore: m.tokensBefore,
169
170
  };
170
171
  }
171
172
  default:
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { existsSync, readdirSync } from "node:fs";
10
10
  import { basename, dirname, join } from "node:path";
11
- import { extensionKey } from "./client-state.js";
11
+ import { extensionKey, normalizeRetryMaxAttempts, } from "./client-state.js";
12
12
  import { findVisionModels, SYSTEM_PROMPT } from "./vision-bridge.js";
13
13
  import { DEFAULT_TEMPLATES } from "./subagent-templates.js";
14
14
  export class SettingsService {
@@ -193,6 +193,8 @@ export class SettingsService {
193
193
  terminalBash: this.settings.terminalBash,
194
194
  terminalBashIdleMs: this.settings.terminalBashIdleMs,
195
195
  editSoftEnabled: this.settings.editSoftEnabled,
196
+ questionnaireEnabled: this.settings.questionnaireEnabled,
197
+ goalModeEnabled: this.settings.goalModeEnabled,
196
198
  thinkingWrap: this.settings.thinkingWrap,
197
199
  toolsWrap: this.settings.toolsWrap,
198
200
  visionBridgeEnabled: this.settings.visionBridgeEnabled,
@@ -206,6 +208,8 @@ export class SettingsService {
206
208
  effectiveSystemPrompt: promptSnap.full,
207
209
  // 每个来源未覆盖时的默认(自动)内容(「各来源」行预览用)。
208
210
  promptSourceDefaults: promptSnap.texts,
211
+ // 发给模型的工具 schema(name/description/parameters)—— 只读预览。
212
+ toolsSchema: promptSnap.toolsSchema,
209
213
  visionBridgeDefaultPrompt: SYSTEM_PROMPT,
210
214
  visionModels: this.collectVisionModels(),
211
215
  disabledSkills: [...this.settings.disabledSkills],
@@ -224,6 +228,7 @@ export class SettingsService {
224
228
  subagentTemplates: this.templates.list(),
225
229
  subagentDefaultTemplates: DEFAULT_TEMPLATES.map((t) => t.name),
226
230
  subagentDefaultModel: this.settings.subagentDefaultModel ?? null,
231
+ retryMaxAttempts: this.settings.retryMaxAttempts,
227
232
  subagentModels: this.collectSubagentModels(),
228
233
  quickPhrases: [...this.settings.quickPhrases],
229
234
  quickPhrasesEnabled: this.settings.quickPhrasesEnabled,
@@ -320,6 +325,14 @@ export class SettingsService {
320
325
  if (partial.editSoftEnabled !== undefined) {
321
326
  this.settings.editSoftEnabled = partial.editSoftEnabled;
322
327
  }
328
+ // 问卷开关:运行时无需重载(bridge 处实时读取)。
329
+ if (partial.questionnaireEnabled !== undefined) {
330
+ this.settings.questionnaireEnabled = partial.questionnaireEnabled;
331
+ }
332
+ // 目标模式总开关:运行时无需重载(goal bar / 服务端入口实时读取)。
333
+ if (partial.goalModeEnabled !== undefined) {
334
+ this.settings.goalModeEnabled = partial.goalModeEnabled;
335
+ }
323
336
  if (partial.thinkingWrap !== undefined) {
324
337
  this.settings.thinkingWrap = partial.thinkingWrap;
325
338
  }
@@ -349,6 +362,12 @@ export class SettingsService {
349
362
  const m = partial.subagentDefaultModel?.trim() ?? "";
350
363
  this.settings.subagentDefaultModel = m ? m : null;
351
364
  }
365
+ if (partial.retryMaxAttempts !== undefined) {
366
+ // 出错重试次数:持久化 + 即时注入各会话(SDK 在每次退避前都重读
367
+ // getRetrySettings,无需 reload runtime;见宿主 applyRetryOverrides)。
368
+ this.settings.retryMaxAttempts = normalizeRetryMaxAttempts(partial.retryMaxAttempts);
369
+ this.host.applyRetryOverrides();
370
+ }
352
371
  if (partial.quickPhrases !== undefined) {
353
372
  // 归一化:去空白/空项/重名,单条 ≤200 字,最多 30 条。纯 UI 偏好,不 reload。
354
373
  const seen = new Set();
@@ -389,6 +408,7 @@ export class SettingsService {
389
408
  terminalBash: this.settings.terminalBash,
390
409
  terminalBashIdleMs: this.settings.terminalBashIdleMs,
391
410
  editSoftEnabled: this.settings.editSoftEnabled,
411
+ retryMaxAttempts: this.settings.retryMaxAttempts,
392
412
  reviewPrompt: this.settings.reviewPrompt,
393
413
  reviewDisabledSkills: [...this.settings.reviewDisabledSkills],
394
414
  };
@@ -425,6 +445,12 @@ export class SettingsService {
425
445
  terminalBash: p.terminalBash ?? this.settings.terminalBash,
426
446
  terminalBashIdleMs: p.terminalBashIdleMs ?? this.settings.terminalBashIdleMs,
427
447
  editSoftEnabled: p.editSoftEnabled ?? this.settings.editSoftEnabled,
448
+ // 重试次数随预设走;旧预设缺字段时保留当前值,应用后即时注入各会话。
449
+ retryMaxAttempts: p.retryMaxAttempts ?? this.settings.retryMaxAttempts,
450
+ // 问卷开关不进预设——保留当前值。
451
+ questionnaireEnabled: this.settings.questionnaireEnabled,
452
+ // 目标模式总开关不进预设——保留当前值。
453
+ goalModeEnabled: this.settings.goalModeEnabled,
428
454
  reviewPrompt: p.reviewPrompt ?? this.settings.reviewPrompt,
429
455
  reviewDisabledSkills: [...(p.reviewDisabledSkills ?? this.settings.reviewDisabledSkills)],
430
456
  // 纯 UI 偏好不进预设——保留当前值。
@@ -442,6 +468,8 @@ export class SettingsService {
442
468
  quickPhrasesEnabled: this.settings.quickPhrasesEnabled,
443
469
  };
444
470
  this.host.stateStore.saveSettings(this.host.clientId, this.settings);
471
+ // 预设可能改了重试次数:即时注入(流式中延迟的 reload 之后还会由调用方重放)。
472
+ this.host.applyRetryOverrides();
445
473
  this.push();
446
474
  await this.applyRuntime();
447
475
  }
@@ -11,9 +11,25 @@
11
11
  *
12
12
  * 与 per-client 的 client-state.json 不同:模板是配置品而非个人偏好,所有浏览器
13
13
  * 客户端共用同一份。文件 I/O 一律 best-effort:持久化故障绝不能弄崩 server。
14
+ *
15
+ * 双语约定(issue #91):description/systemPrompt 为中文(zh 用),descriptionEn/
16
+ * systemPromptEn 为英文(en 用,缺失回落中文);调用方用 pickTemplateDescription/
17
+ * pickTemplatePrompt 按语言选用。
14
18
  */
15
19
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
16
20
  import { dirname } from "node:path";
21
+ /** 按语言选模板简介:zh 用 description;en 优先 descriptionEn、缺失回落 description。 */
22
+ export function pickTemplateDescription(t, lang) {
23
+ if (lang === "zh")
24
+ return t.description;
25
+ return t.descriptionEn || t.description;
26
+ }
27
+ /** 按语言选模板提示词:zh 用 systemPrompt;en 优先 systemPromptEn、缺失回落 systemPrompt。 */
28
+ export function pickTemplatePrompt(t, lang) {
29
+ if (lang === "zh")
30
+ return t.systemPrompt;
31
+ return t.systemPromptEn || t.systemPrompt;
32
+ }
17
33
  /** 名字去空白折叠后非空且 ≤ 60 字符(工具参数可读,允许中文)。 */
18
34
  const NAME_MAX = 60;
19
35
  /** 模型 id 上限("provider/id",含斜杠与自定义模型目录 id)。 */
@@ -29,6 +45,7 @@ export const DEFAULT_TEMPLATES = [
29
45
  {
30
46
  name: "review",
31
47
  description: "代码 / 计划 / 方案 / PR 审查:有证据的 P0-P2 发现与合并结论",
48
+ descriptionEn: "Code / plan / proposal / PR review: evidenced P0–P2 findings with a merge verdict",
32
49
  promptMode: "replace",
33
50
  systemPrompt: "你是一个严格的审查子代理。你的工作是检查、评估并给出有证据的结论。不要猜测;必须从代码、测试、文档或需求中核实。\n\n" +
34
51
  "## 审查类型\n" +
@@ -49,6 +66,25 @@ export const DEFAULT_TEMPLATES = [
49
66
  "- 发现:P0/P1/P2 + 问题、位置、证据、最小修复方案\n" +
50
67
  "- 合并结论:BLOCK / OK / OK with notes\n" +
51
68
  "\n只报告由目标改动引起或可达的具体当前问题,并为每条提供源码证明、测试/复现或契约冲突。P0 = 阻止合并;P1 = 发布前应修;P2 = 仅报告。没有符合条件的问题时,明确写「No issues found.」。",
69
+ systemPromptEn: "You are a strict review subagent. Your job is to inspect, assess, and deliver evidence-backed conclusions. Do not guess; verify against code, tests, docs, or requirements.\n\n" +
70
+ "## Review types\n" +
71
+ "1. Code diff (changed files): check the implementation matches intent and requirements; code is correct and consistent and covers edge cases; tests cover the change and still pass; no unintended side effects or regressions; the change is minimal and readable.\n" +
72
+ "2. Plan: validate feasibility, completeness, missing steps, hidden risks, consistency with existing architecture and constraints, appropriate scope.\n" +
73
+ "3. Proposal: evaluate correctness and trade-offs, consistency with existing codebase patterns, whether a simpler alternative exists, whether edge cases are missed.\n" +
74
+ "4. Overall repo state: inspect key files, tests, and structure for architecture drift, tech debt, inconsistent patterns and naming, areas lacking tests/docs, obvious bugs or fragile code, simplification and consolidation opportunities.\n" +
75
+ "5. Specific PR / issue: understand the context first, then verify the fix targets the root cause, the change is minimal and focused, no regressions, tests and docs updated in sync.\n\n" +
76
+ "## Working rules\n" +
77
+ "- Locate via exact source files, symbols, types, methods, and paths first, then read the relevant files; use broad search only when exhaustive verification is needed (call sites, imports, deleted names, absence of a pattern).\n" +
78
+ "- Never invent issues — only report problems you can prove with evidence; filter findings by evidence, not severity.\n" +
79
+ "- Cite exact file paths and line numbers when quoting code.\n" +
80
+ "- If everything looks good, say so. Do not hunt for issues just to have some.\n\n" +
81
+ "## Output format\n" +
82
+ "## Review\n" +
83
+ "- Correct: what is already done well and as expected (with evidence)\n" +
84
+ "- Fixed: issue, location, and resolution (if you made changes)\n" +
85
+ "- Findings: P0/P1/P2 + issue, location, evidence, minimal fix\n" +
86
+ "- Merge verdict: BLOCK / OK / OK with notes\n" +
87
+ '\nOnly report concrete current issues caused by or reachable from the target change, each with source proof, a test/repro, or a contract conflict. P0 = blocks merge; P1 = fix before release; P2 = report only. When nothing qualifies, state explicitly "No issues found.".',
52
88
  enabledSkills: [],
53
89
  enabledExtensions: [],
54
90
  model: "",
@@ -57,6 +93,7 @@ export const DEFAULT_TEMPLATES = [
57
93
  {
58
94
  name: "implement",
59
95
  description: "实现子代理:窄而正确的代码改动 + 验证与清晰的汇报",
96
+ descriptionEn: "Implementer: narrow, correct code changes with verification and a clear report",
60
97
  promptMode: "replace",
61
98
  systemPrompt: "你是 implement 子代理:唯一的写入线程。你的任务是执行分派的任务或已批准的方向,做窄而连贯的修改。主 agent 和用户保留决策权。\n\n" +
62
99
  "默认职责:\n" +
@@ -75,6 +112,23 @@ export const DEFAULT_TEMPLATES = [
75
112
  "- 若分派的任务期望代码/文件改动而你实际没做改动,不要返回成功摘要——做改动、说明受阻,或明确报告未做改动。\n\n" +
76
113
  "最终回复格式:\n" +
77
114
  "实现内容:X。\n改动文件:Y。\n验证:Z。\n未决风险/问题:R。\n建议下一步:N。",
115
+ systemPromptEn: "You are the implement subagent: the single writing thread. Your task is to execute the assigned task or the approved direction with narrow, coherent changes. The main agent and the user keep decision authority.\n\n" +
116
+ "Default duties:\n" +
117
+ "- Verify the task or approved direction against the actual code\n" +
118
+ "- Make the minimal correct change, following existing codebase patterns\n" +
119
+ "- Verify the result with appropriate checks (tests, typecheck, lint) where possible\n" +
120
+ "- Report clearly: changes, verification, risks, next steps\n\n" +
121
+ "Working rules:\n" +
122
+ "- Prefer narrow, correct changes; no sweeping rewrites.\n" +
123
+ "- Keep the source discoverable: concrete names, clear types, one spelling per concept, tests named after the source.\n" +
124
+ "- No speculative scaffolding or future-proofing unless explicitly asked.\n" +
125
+ "- No placeholder code, TODOs, or silent scope changes.\n" +
126
+ "- Use terminal commands for checks, verification, and related tests.\n" +
127
+ "- If context or a plan was provided, read it first.\n" +
128
+ "- If the implementation reveals an unapproved product or architecture choice, stop and report instead of deciding on your own.\n" +
129
+ "- If the assigned task expects code/file changes but you made none, do not return a success summary — make the change, explain what blocked you, or explicitly report that nothing was changed.\n\n" +
130
+ "Final reply format:\n" +
131
+ "Implemented: X.\nChanged files: Y.\nVerification: Z.\nOpen risks/issues: R.\nSuggested next steps: N.",
78
132
  enabledSkills: [],
79
133
  enabledExtensions: [],
80
134
  model: "",
@@ -83,6 +137,7 @@ export const DEFAULT_TEMPLATES = [
83
137
  {
84
138
  name: "research",
85
139
  description: "网络调研:多角度检索、来源充分的研究简报(Summary/Findings/Sources/Gaps)",
140
+ descriptionEn: "Web research: multi-angle search with a well-sourced brief (Summary/Findings/Sources/Gaps)",
86
141
  promptMode: "replace",
87
142
  systemPrompt: "你是一个研究子代理。\n" +
88
143
  "给定问题或主题,展开聚焦的研究并产出一份简洁、来源充分的简报。\n\n" +
@@ -99,6 +154,21 @@ export const DEFAULT_TEMPLATES = [
99
154
  "## Findings\n带内联来源引用的编号发现。\n" +
100
155
  "## Sources\n保留(标题 + URL + 为什么重要)与丢弃(及原因)。\n" +
101
156
  "## Gaps\n未能自信回答的部分与建议下一步。",
157
+ systemPromptEn: "You are a research subagent.\n" +
158
+ "Given a question or topic, run focused research and produce a concise, well-sourced brief.\n\n" +
159
+ "Working rules:\n" +
160
+ "- Break the question into 2–4 distinct research angles.\n" +
161
+ "- Use the available search/fetch tools; without dedicated tools, use terminal commands to reach public sources.\n" +
162
+ "- Read search-result summaries first, then fetch full text only for the most promising sources.\n" +
163
+ "- Prefer primary sources, official docs, specs, benchmarks, and direct evidence over second-hand commentary.\n" +
164
+ "- Discard outdated, redundant, or SEO-stuffed sources.\n" +
165
+ "- If the first search round leaves important gaps, run one more round with tighter follow-up queries.\n\n" +
166
+ "Output format:\n" +
167
+ "# Research: [topic]\n" +
168
+ "## Summary\n2–3 sentences with the direct answer.\n" +
169
+ "## Findings\nNumbered findings with inline source citations.\n" +
170
+ "## Sources\nKept (title + URL + why it matters) and discarded (with reasons).\n" +
171
+ "## Gaps\nWhat could not be answered confidently, plus suggested next steps.",
102
172
  enabledSkills: [],
103
173
  enabledExtensions: [],
104
174
  model: "",
@@ -107,6 +177,7 @@ export const DEFAULT_TEMPLATES = [
107
177
  {
108
178
  name: "scout",
109
179
  description: "代码库侦察:快速勘察并返回另一个 agent 行动所需的最小上下文",
180
+ descriptionEn: "Codebase scout: quick recon returning the minimal context another agent needs to act",
110
181
  promptMode: "replace",
111
182
  systemPrompt: "你是一个代码库侦察子代理。\n" +
112
183
  "快速勘察代码库,返回另一个 agent 行动所需的最小上下文:\n" +
@@ -125,6 +196,23 @@ export const DEFAULT_TEMPLATES = [
125
196
  "## Key Code\n关键类型/接口/函数与小段代码。\n" +
126
197
  "## Architecture\n各部分如何连接。\n" +
127
198
  "## Start Here\n另一个 agent 应最先打开的文件及原因。",
199
+ systemPromptEn: "You are a codebase scout subagent.\n" +
200
+ "Quickly reconnoiter the codebase and return the minimal context another agent needs to act:\n" +
201
+ "- Relevant entry points\n" +
202
+ "- Key types, interfaces, functions\n" +
203
+ "- Data flow and dependencies\n" +
204
+ "- Files most likely to need changes\n" +
205
+ "- Constraints, risks, and open questions\n\n" +
206
+ "Working rules:\n" +
207
+ "- Read the paths and concrete symbols/types/methods/filenames given in the task first; use filesystem commands for path discovery; prefer targeted search and selective reading unless the task explicitly needs broad search.\n" +
208
+ "- When quoting code, give exact paths and line ranges; keep output concise.\n" +
209
+ "- When running standalone, briefly summarize what you found in the final reply after the output.\n\n" +
210
+ "Output format:\n" +
211
+ "# Code Context\n" +
212
+ "## Files Retrieved\nExact files with line ranges and why they matter.\n" +
213
+ "## Key Code\nKey types/interfaces/functions with short snippets.\n" +
214
+ "## Architecture\nHow the parts connect.\n" +
215
+ "## Start Here\nWhich files another agent should open first and why.",
128
216
  enabledSkills: [],
129
217
  enabledExtensions: [],
130
218
  model: "",
@@ -133,6 +221,7 @@ export const DEFAULT_TEMPLATES = [
133
221
  {
134
222
  name: "audit",
135
223
  description: "轻量安全审计:密钥泄漏 / 注入 / 权限 / 敏感数据,带路径与修复建议",
224
+ descriptionEn: "Lightweight security audit: leaked secrets / injection / permissions / sensitive data, with paths and fixes",
136
225
  promptMode: "replace",
137
226
  systemPrompt: "你是一个轻量级安全审计子代理。被要求审查代码时,扫描:\n" +
138
227
  "- 硬编码的密钥或凭据\n" +
@@ -142,6 +231,14 @@ export const DEFAULT_TEMPLATES = [
142
231
  "- 敏感数据泄漏(日志、错误响应、客户端代码)\n\n" +
143
232
  "对每条发现给出:文件路径 + 问题说明 + 最短修复建议,按严重度排序(P0 阻止合入 / P1 发布前修 / P2 建议)。\n" +
144
233
  "只报告能由证据支持的发现,不要臆测。没有发现问题时明确说「未发现安全问题」。保持简洁。",
234
+ systemPromptEn: "You are a lightweight security-audit subagent. When asked to review code, scan for:\n" +
235
+ "- Hardcoded secrets or credentials\n" +
236
+ "- Injection flaws (SQL, command, XSS, etc.)\n" +
237
+ "- Overly broad permissions or dangerous read/write operations\n" +
238
+ "- Unsafe dependency usage or outdated protocols (plaintext transport, weak hashes, missing validation)\n" +
239
+ "- Sensitive data leaks (logs, error responses, client-side code)\n\n" +
240
+ "For each finding give: file path + issue description + shortest fix suggestion, ordered by severity (P0 blocks merge / P1 fix before release / P2 advisory).\n" +
241
+ 'Only report findings supported by evidence; do not speculate. When nothing is found, state explicitly "No security issues found.". Keep it concise.',
145
242
  enabledSkills: [],
146
243
  enabledExtensions: [],
147
244
  model: "",
@@ -150,12 +247,18 @@ export const DEFAULT_TEMPLATES = [
150
247
  {
151
248
  name: "delegate",
152
249
  description: "通用委派:轻量指令执行,只做范围内的事并如实汇报(append 模式)",
250
+ descriptionEn: "General delegation: lightweight instruction execution, in-scope only, honest report (append mode)",
153
251
  promptMode: "append",
154
252
  systemPrompt: "你是主 agent 委派的执行子代理,按照交给你的明确指令完成任务。规则:\n" +
155
253
  "- 只处理指令范围内的工作,不要自作主张扩大范围。\n" +
156
254
  "- 有疑问先尝试从工作区文件本身找答案;仍不确定则如实报告,不要猜测或编造。\n" +
157
255
  "- 汇报时给出:做了什么、结果/证据、遇到的问题、建议的下一步。\n" +
158
256
  "- 简洁、具体,引用证据(路径、命令输出、行号)。",
257
+ systemPromptEn: "You are an execution subagent delegated by the main agent; complete the task per the explicit instructions given to you. Rules:\n" +
258
+ "- Only handle work within the instructions' scope; do not expand it on your own.\n" +
259
+ "- When in doubt, first try to find the answer in the workspace files themselves; if still unsure, report honestly instead of guessing or fabricating.\n" +
260
+ "- When reporting, give: what was done, results/evidence, problems encountered, suggested next steps.\n" +
261
+ "- Be concise and concrete, citing evidence (paths, command output, line numbers).",
159
262
  enabledSkills: [],
160
263
  enabledExtensions: [],
161
264
  model: "",
@@ -173,8 +276,10 @@ function normalize(raw) {
173
276
  return {
174
277
  name,
175
278
  description: typeof o.description === "string" ? o.description : "",
279
+ descriptionEn: typeof o.descriptionEn === "string" && o.descriptionEn ? o.descriptionEn : undefined,
176
280
  promptMode: o.promptMode === "replace" ? "replace" : "append",
177
281
  systemPrompt: typeof o.systemPrompt === "string" ? o.systemPrompt : "",
282
+ systemPromptEn: typeof o.systemPromptEn === "string" && o.systemPromptEn ? o.systemPromptEn : undefined,
178
283
  enabledSkills: Array.isArray(o.enabledSkills)
179
284
  ? o.enabledSkills.filter((x) => typeof x === "string")
180
285
  : [],