pi-web-ui 0.92.0 → 0.93.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 (56) hide show
  1. package/CHANGELOG.md +58 -2
  2. package/README.md +69 -14
  3. package/README.zh-CN.md +36 -10
  4. package/bin/pi-web-ui.mjs +18 -6
  5. package/dist/server/agent-service.js +497 -120
  6. package/dist/server/auth-cookie.js +26 -0
  7. package/dist/server/claim-files-tool.js +6 -7
  8. package/dist/server/client-state.js +65 -6
  9. package/dist/server/conversation-read-tool.js +2 -1
  10. package/dist/server/delegate-task.js +10 -3
  11. package/dist/server/dsh/dsh-agent-service.js +43 -1
  12. package/dist/server/files-service.js +78 -26
  13. package/dist/server/http-proxy.js +72 -0
  14. package/dist/server/index.js +62 -7
  15. package/dist/server/patch-node-pty.js +11 -0
  16. package/dist/server/plugins.js +1 -0
  17. package/dist/server/protocol-version.js +1 -1
  18. package/dist/server/read-tool.js +37 -3
  19. package/dist/server/resolve-global-sdk.js +72 -0
  20. package/dist/server/sdk-origin.js +64 -0
  21. package/dist/server/serialize.js +59 -21
  22. package/dist/server/settings-service.js +12 -1
  23. package/dist/server/skill-tool.js +2 -1
  24. package/dist/server/subagents.js +30 -13
  25. package/dist/server/terminals.js +38 -18
  26. package/dist/server/themes.js +15 -0
  27. package/dist/server/tool-info.js +89 -0
  28. package/dist/server/tool-manager.js +107 -14
  29. package/package.json +2 -2
  30. package/plugins/catalog.json +10 -0
  31. package/themes/ayu-light.css +136 -0
  32. package/themes/catppuccin-latte.css +4 -0
  33. package/themes/catppuccin.css +4 -0
  34. package/themes/codex.css +136 -0
  35. package/themes/cyberpunk.css +4 -0
  36. package/themes/dazzle.css +4 -0
  37. package/themes/everforest-light.css +136 -0
  38. package/themes/geist.css +136 -0
  39. package/themes/gruvbox-light.css +136 -0
  40. package/themes/kanagawa-lotus.css +136 -0
  41. package/themes/md-preview.css +4 -0
  42. package/themes/mist.css +4 -0
  43. package/themes/nord.css +4 -0
  44. package/themes/one-dark.css +4 -0
  45. package/themes/paper.css +4 -0
  46. package/themes/rose-pine-dawn.css +136 -0
  47. package/themes/sakura.css +4 -0
  48. package/themes/solarized-light.css +4 -0
  49. package/themes/tokyo-night.css +4 -0
  50. package/themes/white.css +4 -0
  51. package/web/dist/assets/{TerminalPanel-RACpk6AX.js → TerminalPanel-C1A23aI-.js} +1 -1
  52. package/web/dist/assets/index-B48QWf0I.js +364 -0
  53. package/web/dist/assets/index-DGL5OInG.css +1 -0
  54. package/web/dist/index.html +2 -2
  55. package/web/dist/assets/index-BgrnP9Jz.js +0 -364
  56. package/web/dist/assets/index-Cy8dSTZt.css +0 -1
@@ -21,6 +21,7 @@ import { statSync, readFileSync } from "node:fs";
21
21
  import { defineTool } from "@earendil-works/pi-coding-agent";
22
22
  import { Type } from "typebox";
23
23
  import { bilingual, pick } from "./i18n.js";
24
+ import { SKILL_TOOL_NAME } from "./tool-manager.js";
24
25
  import { decodeText } from "./text-sniff.js";
25
26
  /** 单文件正文上限(与 agent-service fillSkillContents 同口径:8KB)。 */
26
27
  export const SKILL_TOOL_FILE_CAP = 8192;
@@ -58,7 +59,7 @@ export function makeSkillTool(host, lang) {
58
59
  details,
59
60
  });
60
61
  return defineTool({
61
- name: "skill",
62
+ name: SKILL_TOOL_NAME,
62
63
  label: "Load a skill",
63
64
  description: bilingual("Load a skill's full text by its exact name (see the <available_skills> catalog in the system prompt). " +
64
65
  "Prefer this over reading the skill file with the read tool — no path guessing needed. " +
@@ -61,7 +61,7 @@ export function subagentTitle(prompt) {
61
61
  export function withSubagentOwner(host, ownerId) {
62
62
  return {
63
63
  ...host,
64
- spawnSubagent: (prompt, type, cwd, templateName, model) => host.spawnSubagent(prompt, type, cwd, templateName, model, ownerId),
64
+ spawnSubagent: (prompt, type, cwd, templateName, model, _parentId, persist) => host.spawnSubagent(prompt, type, cwd, templateName, model, ownerId, persist),
65
65
  };
66
66
  }
67
67
  /**
@@ -125,6 +125,11 @@ export function makeSubagentTools(host, lang, selfConvId) {
125
125
  cwd: Type.Optional(Type.String({
126
126
  description: bilingual("Subagent working directory (relative/absolute). Defaults to the main session's cwd.", "子代理工作目录(相对/绝对)。默认继承主会话工作目录。"),
127
127
  })),
128
+ persist: Type.Optional(Type.Boolean({
129
+ description: bilingual("Optional: persist this conversation to disk as a regular session (saved in history, resumable). " +
130
+ "Default false (lightweight in-memory subagent). Use true for tasks that need long-term retention or human follow-up.", "可选:是否将该对话持久化落盘为普通对话(保存在历史会话中,可随时回顾与继续)。" +
131
+ "默认 false(轻量内存子代理)。需要长期留存或后续人工跟进的任务建议设为 true。"),
132
+ })),
128
133
  }),
129
134
  execute: async (_id, p, _signal, _onUpdate, ctx) => {
130
135
  if (p.template && !host.isTemplateUsable(p.template)) {
@@ -134,7 +139,7 @@ export function makeSubagentTools(host, lang, selfConvId) {
134
139
  // host 抛错):转成返回文本而不是直接抛,让 AI 能读到原因并调整重试。
135
140
  let convId;
136
141
  try {
137
- convId = await host.spawnSubagent(p.prompt, p.type ?? "general", p.cwd ?? ctx.cwd, p.template, p.model);
142
+ convId = await host.spawnSubagent(p.prompt, p.type ?? "general", p.cwd ?? ctx.cwd, p.template, p.model, undefined, p.persist);
138
143
  }
139
144
  catch (err) {
140
145
  const msg = err instanceof Error ? err.message : String(err);
@@ -151,8 +156,10 @@ export function makeSubagentTools(host, lang, selfConvId) {
151
156
  const templateLineEn = p.template ? `\nTemplate: ${p.template}` : "";
152
157
  const modelLineZh = p.model ? `\n模型:${p.model}` : "";
153
158
  const modelLineEn = p.model ? `\nModel: ${p.model}` : "";
154
- return text(pick(getLang(), `子代理已启动(运行列表可见):${convId}\n类型:${subagentType} · 标题:${subagentTitleText}${templateLineZh}${modelLineZh}` +
155
- `\n用 subagent_wait_all 一次等全部完成(不用轮询),subagent_get_result 取单个结果,subagent_list 看运行态,subagent_steer 改向,subagent_stop 停止。`, `Subagent started (visible in the running list): ${convId}\nType: ${subagentType} · Title: ${subagentTitleText}${templateLineEn}${modelLineEn}` +
159
+ const kindLabelZh = p.persist ? "普通持久化对话" : "子代理";
160
+ const kindLabelEn = p.persist ? "Persistent conversation" : "Subagent";
161
+ return text(pick(getLang(), `${kindLabelZh}已启动(运行列表可见):${convId}\n类型:${subagentType} · 标题:${subagentTitleText}${templateLineZh}${modelLineZh}` +
162
+ `\n用 subagent_wait_all 一次等全部完成(不用轮询),subagent_get_result 取单个结果,subagent_list 看运行态,subagent_steer 改向,subagent_stop 停止。`, `${kindLabelEn} started (visible in the running list): ${convId}\nType: ${subagentType} · Title: ${subagentTitleText}${templateLineEn}${modelLineEn}` +
156
163
  `\nUse subagent_wait_all to wait for all at once (no polling), subagent_get_result for a single result, subagent_list for live status, subagent_steer to redirect, subagent_stop to stop.`, "subagents.spawn.started", {
157
164
  convId: convId,
158
165
  subagentType: subagentType,
@@ -163,7 +170,7 @@ export function makeSubagentTools(host, lang, selfConvId) {
163
170
  templateLineEn: templateLineEn,
164
171
  modelLineZh: modelLineZh,
165
172
  modelLineEn: modelLineEn,
166
- }), { convId, template: p.template, model: p.model });
173
+ }), { convId, template: p.template, model: p.model, persisted: !!p.persist });
167
174
  },
168
175
  }),
169
176
  defineTool({
@@ -220,16 +227,24 @@ export function makeSubagentTools(host, lang, selfConvId) {
220
227
  defineTool({
221
228
  name: "subagent_list",
222
229
  label: "List subagents",
223
- description: bilingual("List all subagents and their live status: convId, type, state, title, message count (errors/aborts are marked in the state).", "列出全部子代理的运行态:convId、类型、状态、标题、消息数(报错/中止的会在状态里标出)。"),
230
+ description: bilingual("List all subagents and managed conversations with their live status: convId, type, state, title, message count (errors/aborts are marked in the state).", "列出全部子代理及受控对话的运行态:convId、类型、状态、标题、消息数(报错/中止的会在状态里标出)。"),
224
231
  promptSnippet: "list all subagents and their live status",
225
- parameters: Type.Object({}),
226
- execute: async () => {
227
- const list = host.listSubagents();
232
+ parameters: Type.Object({
233
+ kind: Type.Optional(Type.String({
234
+ enum: ["all", "subagent", "persistent"],
235
+ description: bilingual("Filter: all (default) = all managed tasks; subagent = only ephemeral in-memory subagents; persistent = only persistent conversations.", "过滤类型:all(默认)= 全部受控对话;subagent = 仅临时内存子代理;persistent = 仅持久化普通对话。"),
236
+ })),
237
+ }),
238
+ execute: async (_id, p) => {
239
+ const list = host.listSubagents(p.kind);
228
240
  if (list.length === 0)
229
- return text(pick(getLang(), "当前没有子代理。", "No subagents running.", "subagents.list.empty"));
241
+ return text(pick(getLang(), "当前没有运行中的对话/子代理。", "No managed conversations or subagents running.", "subagents.list.empty"));
230
242
  const tLang = getLang();
231
- const lines = list.map((r) => `- ${r.convId} · ${r.type} · ${subagentVerdict(r, tLang)} · ${r.title}` +
232
- (tLang === "zh" ? `(msg: ${r.messageCount})` : ` (msg: ${r.messageCount})`));
243
+ const lines = list.map((r) => {
244
+ const tag = r.persisted ? (tLang === "zh" ? "持久化" : "persistent") : tLang === "zh" ? "子代理" : "subagent";
245
+ return (`- ${r.convId} · ${r.type} · ${subagentVerdict(r, tLang)} · ${r.title}` +
246
+ (tLang === "zh" ? `(${tag} · msg: ${r.messageCount})` : ` (${tag} · msg: ${r.messageCount})`));
247
+ });
233
248
  return text(lines.join("\n"));
234
249
  },
235
250
  }),
@@ -374,7 +389,9 @@ export function makeSubagentTools(host, lang, selfConvId) {
374
389
  const emptyLang = getLang();
375
390
  return text(pick(emptyLang, "没有需要等待的子代理(调用者自身与祖先不计入等待;子代理无参时只等自己的后代)。", "No subagents to wait for (the calling session itself and its ancestors are never waited on; a subagent without runIds only waits for its own descendants).", "subagents.wait.empty"));
376
391
  }
377
- const timeoutMs = Math.min(Math.max(p.timeoutSeconds ?? 600, 1), Math.floor(WAIT_CAP_MS / 1000)) * 1000;
392
+ const currentWatchdogMs = host.getWatchdogTimeoutMs?.() ?? WAIT_CAP_MS;
393
+ const currentWaitCapMs = currentWatchdogMs > 0 ? Math.max(60_000, Math.floor(currentWatchdogMs * 0.8)) : 3600_000;
394
+ const timeoutMs = Math.min(Math.max(p.timeoutSeconds ?? 600, 1), Math.floor(currentWaitCapMs / 1000)) * 1000;
378
395
  const waitStart = Date.now();
379
396
  const deadline = waitStart + timeoutMs;
380
397
  // 已到终态的、(或已被移出找不到的)直接归位;剩下的阻塞轮询到
@@ -1295,10 +1295,16 @@ export class TerminalManager {
1295
1295
  w.cb(null);
1296
1296
  entry.exitCode = exitCode;
1297
1297
  this.terms.delete(id);
1298
+ // 进程已退出,立即释放底层的 PTY 句柄(Windows ConPTY / HPCON / sockets 等),
1299
+ // 避免已退出的终端长期占用 ConPTY 与 MSYS2 控制台资源(issue #269)。
1300
+ this.killNative(entry);
1298
1301
  while (this.history.size >= MAX_TERMINAL_HISTORY) {
1299
1302
  const oldest = this.history.keys().next().value;
1300
1303
  if (typeof oldest !== "string")
1301
1304
  break;
1305
+ const oldEntry = this.history.get(oldest);
1306
+ if (oldEntry)
1307
+ this.killNative(oldEntry);
1302
1308
  this.history.delete(oldest);
1303
1309
  }
1304
1310
  this.history.set(id, entry);
@@ -1339,8 +1345,13 @@ export class TerminalManager {
1339
1345
  this.emitList();
1340
1346
  return;
1341
1347
  }
1342
- if (this.history.delete(id))
1348
+ const histEntry = this.history.get(id);
1349
+ if (histEntry) {
1350
+ this.killNative(histEntry);
1351
+ this.history.delete(id);
1343
1352
  this.emitList();
1353
+ return;
1354
+ }
1344
1355
  }
1345
1356
  /** Rename a terminal tab (live or retained history). Empty names ignored. */
1346
1357
  rename(id, title) {
@@ -1354,27 +1365,33 @@ export class TerminalManager {
1354
1365
  this.emitList();
1355
1366
  }
1356
1367
  /**
1357
- * Kill the native PTY (issue #215:Windows ConPTY 关机死锁)。
1368
+ * Kill the native PTY (issue #215:Windows ConPTY 关机死锁;issue #269:MSYS2 控制台耗尽死锁)。
1358
1369
  *
1359
- * Windows 下 `pty.kill()` 底层走 `ClosePseudoConsole`,管道有未排空数据时会
1360
- * 内核级同步死锁,直接冻住单线程事件循环(进程内 setTimeout 看门狗与二次
1361
- * Ctrl+C 全灭)。所以 Windows 先走 Node 原生的 `process.kill(pid)`
1362
- *(TerminateProcess,从不阻塞)把 shell 干掉:
1363
- * - 关机(shutdown=true):到此为止,不再调 `pty.kill()`。进程马上就
1364
- * `process.exit`,HPC 句柄由 OS 回收;再调只会自找死锁。
1365
- * - 日常(tab 关闭/移出对话):子进程已死、管道见 EOF 后再调 `pty.kill()`
1366
- * 释放 HPC 句柄,此时不再阻塞(长驻服务必须释放句柄,不能像关机那样一走了之)。
1367
- * 非 Windows 原样直调 `pty.kill()`。
1370
+ * Windows 下:
1371
+ * 1. 若进程已经退出(entry.exited):绝不可再调用 Node 的 process.kill(pid),
1372
+ * 直接调 pty.kill() 释放 HPCON 句柄(子进程已死、管道见 EOF,绝不会死锁)。
1373
+ * 2. 若进程尚未退出(!entry.exited):
1374
+ * 先尝试优雅关闭(向 PTY 写入 \x03exit\r),让 bash 正常触发清理钩子;
1375
+ * 若仍未退出才以 process.kill(pid)(TerminateProcess)强制兜底。
1376
+ * 3. 关机(shutdown=true):跳过 pty.kill(),由 OS 回收。
1377
+ * 非 Windows 原样直调 pty.kill()。
1368
1378
  */
1369
1379
  killNative(entry, shutdown = false) {
1370
1380
  if (process.platform === "win32") {
1371
- const pid = entry.pty.pid;
1372
- if (typeof pid === "number") {
1373
- try {
1374
- process.kill(pid);
1375
- }
1376
- catch {
1377
- // already dead
1381
+ if (!entry.exited) {
1382
+ const pid = entry.pty.pid;
1383
+ if (typeof pid === "number") {
1384
+ try {
1385
+ // 优雅关闭:先发 Ctrl+C,再发 exit\r
1386
+ entry.pty.write("\x03exit\r");
1387
+ }
1388
+ catch { }
1389
+ try {
1390
+ process.kill(pid);
1391
+ }
1392
+ catch {
1393
+ // already dead
1394
+ }
1378
1395
  }
1379
1396
  }
1380
1397
  if (shutdown)
@@ -1410,6 +1427,9 @@ export class TerminalManager {
1410
1427
  w.cb(null);
1411
1428
  entry.watches = [];
1412
1429
  }
1430
+ for (const entry of this.history.values()) {
1431
+ this.killNative(entry, shutdown);
1432
+ }
1413
1433
  this.terms.clear();
1414
1434
  this.history.clear();
1415
1435
  this.emitList();
@@ -23,6 +23,20 @@ const ID_RE = /^[A-Za-z0-9_-]+$/;
23
23
  const THEME_NAME_RE = /\/\*\s*theme-name:\s*(.+?)\s*\*\//;
24
24
  const THEME_NAME_EN_RE = /\/\*\s*theme-name-en:\s*(.+?)\s*\*\//;
25
25
  const THEME_GROUP_RE = /\/\*\s*theme-group:\s*(.+?)\s*\*\//;
26
+ /** 从主题 CSS 头部读取 color-scheme(浅/暗),无法识别时返回 undefined。 */
27
+ const THEME_SCHEME_RE = /color-scheme:\s*(dark|light)/i;
28
+ function readScheme(path) {
29
+ try {
30
+ const head = readFileSync(path, "utf8").slice(0, 800);
31
+ const m = head.match(THEME_SCHEME_RE);
32
+ if (m && (m[1] === "dark" || m[1] === "light"))
33
+ return m[1];
34
+ }
35
+ catch {
36
+ // ignore
37
+ }
38
+ return undefined;
39
+ }
26
40
  function readDisplayName(path, fallback) {
27
41
  try {
28
42
  const head = readFileSync(path, "utf8").slice(0, 400);
@@ -72,6 +86,7 @@ export function listThemes(builtinDir, userDir) {
72
86
  name,
73
87
  builtin,
74
88
  group,
89
+ scheme: readScheme(path),
75
90
  ...(nameEn ? { nameEn } : {}),
76
91
  };
77
92
  });
@@ -0,0 +1,89 @@
1
+ /** parameters(JSON Schema)的体积上限:超过就整丢。 */
2
+ export const TOOL_PARAMETERS_CAP = 64_000;
3
+ /** description 的字数上限(SDK 里的工具描述很少超过几百字,这里防的是扩展写疯)。 */
4
+ export const TOOL_DESCRIPTION_CAP = 8_000;
5
+ /** promptGuidelines 合计字数上限(进系统提示词的要点,正常都很短)。 */
6
+ export const TOOL_GUIDELINES_CAP = 8_000;
7
+ function str(v) {
8
+ if (typeof v !== "string")
9
+ return undefined;
10
+ const s = v.trim();
11
+ return s ? s : undefined;
12
+ }
13
+ /** 超长截断(`…` 结尾,与前端 truncateText 同款标记)。 */
14
+ function cut(s, cap) {
15
+ return s.length > cap ? `${s.slice(0, cap)}…` : s;
16
+ }
17
+ /** 参数 schema 是否可下发(能序列化且不超上限)。 */
18
+ function parametersWithinCap(params) {
19
+ if (params === undefined || params === null)
20
+ return false;
21
+ try {
22
+ const text = JSON.stringify(params);
23
+ return typeof text === "string" && text.length <= TOOL_PARAMETERS_CAP;
24
+ }
25
+ catch {
26
+ // 循环引用 / BigInt 等序列化不了的值:当成「没有 schema」。
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * 归一化一条工具定义 → wire 载荷。
32
+ *
33
+ * @param name 请求里的工具名(**永远以它为准**:定义里的 name 缺失时前端仍要能显示标题)。
34
+ * @param raw 引擎给的定义;undefined/null = 没找到。
35
+ */
36
+ export function normalizeToolInfo(name, raw) {
37
+ const toolName = str(name) ?? "";
38
+ if (!raw || typeof raw !== "object")
39
+ return { name: toolName, found: false };
40
+ const payload = { name: toolName, found: true };
41
+ const label = str(raw.label);
42
+ if (label)
43
+ payload.label = cut(label, 200);
44
+ const description = str(raw.description);
45
+ if (description)
46
+ payload.description = cut(description, TOOL_DESCRIPTION_CAP);
47
+ const snippet = str(raw.promptSnippet);
48
+ if (snippet)
49
+ payload.promptSnippet = cut(snippet, TOOL_DESCRIPTION_CAP);
50
+ if (Array.isArray(raw.promptGuidelines)) {
51
+ const guidelines = [];
52
+ let total = 0;
53
+ for (const g of raw.promptGuidelines) {
54
+ const text = str(g);
55
+ if (!text)
56
+ continue;
57
+ if (total + text.length > TOOL_GUIDELINES_CAP)
58
+ break;
59
+ guidelines.push(text);
60
+ total += text.length;
61
+ }
62
+ if (guidelines.length > 0)
63
+ payload.promptGuidelines = guidelines;
64
+ }
65
+ if (raw.parameters !== undefined && raw.parameters !== null) {
66
+ if (parametersWithinCap(raw.parameters))
67
+ payload.parameters = raw.parameters;
68
+ else
69
+ payload.parametersDropped = true;
70
+ }
71
+ if (raw.active === true)
72
+ payload.active = true;
73
+ else if (raw.active === false)
74
+ payload.active = false;
75
+ const info = raw.sourceInfo;
76
+ if (info && typeof info === "object") {
77
+ const source = str(info.source);
78
+ if (source)
79
+ payload.source = source;
80
+ const scope = str(info.scope);
81
+ if (scope)
82
+ payload.scope = scope;
83
+ }
84
+ return payload;
85
+ }
86
+ /** 当前引擎不支持枚举工具定义时的应答(前端据此显示「当前引擎不支持」而不是「未找到」)。 */
87
+ export function unsupportedToolInfo(name) {
88
+ return { name: str(name) ?? "", found: false, unsupported: true };
89
+ }
@@ -61,9 +61,12 @@ export const SCHEDULE_CANCEL_TOOL_NAME = "schedule_cancel";
61
61
  /** 展示文件工具(定义见 present-files-tool.ts):把图片/视频/文本作为预览卡片
62
62
  * 推到对话里,卡片带预览/本地打开/在文件夹中显示/下载/复制路径。 */
63
63
  export const PRESENT_FILES_TOOL_NAME = "present_files";
64
+ /** 文件认领工具(定义见 claim-files-tool.ts):声明要改哪些文件,让同项目的
65
+ * 并行对话绕行(纯建议,不拦编辑)。 */
66
+ export const CLAIM_FILES_TOOL_NAME = "claim_files";
64
67
  /** 旧工具名(持久化迁移用;新代码一律用 MARKERS_LIST_TOOL_NAME)。 */
65
68
  export const LEGACY_MARKERS_LIST_TOOL_NAME = "markers_list";
66
- /** 可开关的 Agent 工具总目录(共 25 个;bash 本体与 SDK 内置 edit/read
69
+ /** 可开关的 Agent 工具总目录(共 26 个;bash 本体与 SDK 内置 edit/read
67
70
  * 不进目录——关了 agent 就残了,不给关)。 */
68
71
  export const AGENT_TOOL_CATALOG = [
69
72
  ...TERMINAL_TOOL_NAMES.map((name) => ({
@@ -78,26 +81,116 @@ export const AGENT_TOOL_CATALOG = [
78
81
  defaultOn: true,
79
82
  dshVisible: false,
80
83
  })),
81
- { name: EDIT_SOFT_TOOL_NAME, group: "other", defaultOn: false, dshVisible: false },
82
- { name: DELEGATE_TASK_TOOL_NAME, group: "other", defaultOn: true, dshVisible: false },
83
- { name: ASK_USER_QUESTION_TOOL_NAME, group: "other", defaultOn: true, dshVisible: true },
84
- { name: MARKERS_LIST_TOOL_NAME, group: "other", defaultOn: true, dshVisible: true },
84
+ {
85
+ name: EDIT_SOFT_TOOL_NAME,
86
+ group: "other",
87
+ defaultOn: false,
88
+ dshVisible: false,
89
+ descKey: "editSoftEnabledDesc",
90
+ offHintKey: "editSoftOffHint",
91
+ },
92
+ {
93
+ name: DELEGATE_TASK_TOOL_NAME,
94
+ group: "other",
95
+ defaultOn: true,
96
+ dshVisible: false,
97
+ descKey: "delegateTaskEnabledDesc",
98
+ offHintKey: "delegateTaskOffHint",
99
+ },
100
+ {
101
+ name: ASK_USER_QUESTION_TOOL_NAME,
102
+ group: "other",
103
+ defaultOn: true,
104
+ dshVisible: true,
105
+ descKey: "questionnaireEnabledDesc",
106
+ offHintKey: "questionnaireOffHint",
107
+ },
85
108
  // 默认关(AI 动用户浏览器,opt-in 才开)且 dshVisible=false:DSH 引擎没有页面桥
86
109
  // (page_request 由 pi 引擎的 customTool 发出),列在那里只会让用户关一个不存在的工具。
87
- { name: BROWSER_PAGE_TOOL_NAME, group: "other", defaultOn: false, dshVisible: false },
110
+ {
111
+ name: BROWSER_PAGE_TOOL_NAME,
112
+ group: "other",
113
+ defaultOn: false,
114
+ dshVisible: false,
115
+ descKey: "browserPageEnabledDesc",
116
+ offHintKey: "browserPageOffHint",
117
+ },
88
118
  // 只读别的对话(含子代理实时消息与历史转录),默认开;DSH 引擎没有该 customTool。
89
- { name: CONVERSATION_READ_TOOL_NAME, group: "other", defaultOn: true, dshVisible: false },
119
+ {
120
+ name: CONVERSATION_READ_TOOL_NAME,
121
+ group: "other",
122
+ defaultOn: true,
123
+ dshVisible: false,
124
+ descKey: "conversationReadEnabledDesc",
125
+ offHintKey: "conversationReadOffHint",
126
+ },
127
+ // 文件认领(事前打招呼,纯 advisory):默认开,不打开 AI 不知道能认领;
128
+ // 关掉只少一路提醒(事后触碰集照常工作);DSH 引擎没有该 customTool
129
+ // (走 goal-rpc,无 customTool 注册面),提醒是服务端算的、DSH 照样能看到。
130
+ {
131
+ name: CLAIM_FILES_TOOL_NAME,
132
+ group: "other",
133
+ defaultOn: true,
134
+ dshVisible: false,
135
+ descKey: "claimFilesEnabledDesc",
136
+ offHintKey: "claimFilesOffHint",
137
+ },
138
+ // 展示文件给用户(图片/视频内联、文本开预览弹窗、本地打开按钮):默认开,
139
+ // 不打开模型根本不知道能“给用户看”;DSH 引擎没有该 customTool(走 shipped preset)。
140
+ {
141
+ name: PRESENT_FILES_TOOL_NAME,
142
+ group: "other",
143
+ defaultOn: true,
144
+ dshVisible: false,
145
+ descKey: "presentFilesEnabledDesc",
146
+ offHintKey: "presentFilesOffHint",
147
+ },
90
148
  // 技能全文按名加载(名录仍在 {{skills}} 段),默认开;DSH 引擎没有该 customTool
91
149
  // (走 goal-rpc,无 customTool 注册面)。
92
- { name: SKILL_TOOL_NAME, group: "other", defaultOn: true, dshVisible: false },
150
+ {
151
+ name: SKILL_TOOL_NAME,
152
+ group: "other",
153
+ defaultOn: true,
154
+ dshVisible: false,
155
+ descKey: "skillEnabledDesc",
156
+ offHintKey: "skillOffHint",
157
+ },
93
158
  // 定时/延时唤醒:默认开(不打开 AI 根本不知道能定时;60s 间隔底线+面板可随时取消),
94
159
  // DSH 引擎没有该 customTool(走 goal-rpc,无 customTool 注册面)。
95
- { name: SCHEDULE_TASK_TOOL_NAME, group: "other", defaultOn: true, dshVisible: false },
96
- { name: SCHEDULE_LIST_TOOL_NAME, group: "other", defaultOn: true, dshVisible: false },
97
- { name: SCHEDULE_CANCEL_TOOL_NAME, group: "other", defaultOn: true, dshVisible: false },
98
- // 展示文件给用户(图片/视频内联、文本开预览弹窗、本地打开按钮):默认开,
99
- // 不打开模型根本不知道能“给用户看”;DSH 引擎没有该 customTool(走 shipped preset)。
100
- { name: PRESENT_FILES_TOOL_NAME, group: "other", defaultOn: true, dshVisible: false },
160
+ {
161
+ name: SCHEDULE_TASK_TOOL_NAME,
162
+ group: "other",
163
+ defaultOn: true,
164
+ dshVisible: false,
165
+ descKey: "scheduleTaskEnabledDesc",
166
+ offHintKey: "scheduleTaskOffHint",
167
+ },
168
+ {
169
+ name: SCHEDULE_LIST_TOOL_NAME,
170
+ group: "other",
171
+ defaultOn: true,
172
+ dshVisible: false,
173
+ descKey: "scheduleTaskEnabledDesc",
174
+ offHintKey: "scheduleTaskOffHint",
175
+ },
176
+ {
177
+ name: SCHEDULE_CANCEL_TOOL_NAME,
178
+ group: "other",
179
+ defaultOn: true,
180
+ dshVisible: false,
181
+ descKey: "scheduleTaskEnabledDesc",
182
+ offHintKey: "scheduleTaskOffHint",
183
+ },
184
+ // todo_list 唯一例外:行不在「其他」组,固定在上面的 markers 分区(设置页循环
185
+ // 跳过它,见 OTHER_AGENT_TOOLS;文案 key 照给,万一哪天搬家不用补)。
186
+ {
187
+ name: MARKERS_LIST_TOOL_NAME,
188
+ group: "other",
189
+ defaultOn: true,
190
+ dshVisible: true,
191
+ descKey: "todoListEnabledDesc",
192
+ offHintKey: "todoListOffHint",
193
+ },
101
194
  ];
102
195
  const KNOWN_NAMES = new Set(AGENT_TOOL_CATALOG.map((t) => t.name));
103
196
  /** 是否为本表登记的可开关工具(未知名一律 false,不抛错)。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.92.0",
3
+ "version": "0.93.0",
4
4
  "description": "Just open your browser to get all your work done — browser cockpit for AI coding agents (chat, code, files, terminal, Git in one tab)",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -84,7 +84,7 @@
84
84
  "dependencies": {
85
85
  "@deepseek-ai/dsh-sdk-jsonrpc-server": "^0.1.1-rc.2",
86
86
  "@deepseek-ai/dsh-sdk-protocol": "^0.1.1-rc.2",
87
- "@earendil-works/pi-coding-agent": "^0.85.1",
87
+ "@earendil-works/pi-coding-agent": ">=0.85.1 <0.87.0",
88
88
  "@xterm/addon-fit": "^0.11.0",
89
89
  "@xterm/xterm": "^6.0.0",
90
90
  "compression": "^1.8.2",
@@ -108,5 +108,15 @@
108
108
  "descriptionEn": "Notes, todos and scheduled reminders in one place: a freely draggable always-on-top panel (position, size and settings all live in it, no separate view tab); reminders fire server-side (survive restarts, delivered on next open), with 5 AI tools and /note /todo /remind quick capture.",
109
109
  "source": "xing-shuyin/pi-web-ui/plugins/notes",
110
110
  "homepage": "https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/notes"
111
+ },
112
+ {
113
+ "id": "pi-web-multigit",
114
+ "name": "multi-git",
115
+ "icon": "🌿",
116
+ "iconSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"18\" r=\"3\"/><circle cx=\"6\" cy=\"6\" r=\"3\"/><circle cx=\"18\" cy=\"6\" r=\"3\"/><path d=\"M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9\"/><path d=\"M12 12v3\"/></svg>",
117
+ "description": "多仓库 Git 总览:工作区内每个仓库的变更、差异、历史、分支、贮藏、跨仓搜索与时间线,另有工作区分支聚合、Fetch/Pull 与内置终端(其余只读)。",
118
+ "descriptionEn": "Multi-repo Git overview: changes, diffs, history, branches, stashes, cross-repo search and a timeline for every repository in the workspace, plus a workspace-wide branch view, Fetch/Pull and an optional terminal.",
119
+ "source": "EinErste/pi-web-multigit",
120
+ "homepage": "https://github.com/EinErste/pi-web-multigit"
111
121
  }
112
122
  ]
@@ -0,0 +1,136 @@
1
+ /* theme-name: 浅阑秋 */
2
+ /* theme-name-en: Ayu Light */
3
+ /* theme-group: classic */
4
+ :root {
5
+ color-scheme: light;
6
+ --bg: #fcfcfc;
7
+ --bg-elev: #f8f9fa;
8
+ --bg-elev2: #f0f1f3;
9
+ --border: #e0e1e3;
10
+ --border-soft: #ecedef;
11
+ --text: #5c6166;
12
+ --text-dim: #66727f;
13
+ --text-faint: #6a7582;
14
+ --accent: #f29718;
15
+ --accent-soft: rgba(242, 151, 24, 0.14);
16
+ --green: #86b300;
17
+ --green-soft: rgba(134, 179, 0, 0.12);
18
+ --red: #f07171;
19
+ --red-soft: rgba(240, 113, 113, 0.1);
20
+ --amber: #e59645;
21
+ --blue: #3b82f6;
22
+ --blue-soft: rgba(59, 130, 246, 0.12);
23
+ --term-bg: #fcfcfc;
24
+ --term-fg: #5c6166;
25
+ --term-cursor: #f29718;
26
+ --term-cursor-accent: #fcfcfc;
27
+ --term-selection: rgba(242, 151, 24, 0.24);
28
+ --term-black: #5c6166;
29
+ --term-red: #f07171;
30
+ --term-green: #86b300;
31
+ --term-yellow: #eba400;
32
+ --term-blue: #22a4e6;
33
+ --term-magenta: #a37acc;
34
+ --term-cyan: #4cbf99;
35
+ --term-white: #f8f9fa;
36
+ --term-bright-black: #828e9f;
37
+ --term-bright-red: #f07171;
38
+ --term-bright-green: #86b300;
39
+ --term-bright-yellow: #eba400;
40
+ --term-bright-blue: #22a4e6;
41
+ --term-bright-magenta: #a37acc;
42
+ --term-bright-cyan: #4cbf99;
43
+ --term-bright-white: #ffffff;
44
+ --mono: "SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
45
+ --mermaid-font-size: 12px;
46
+ --tooltip-bg: #f8f9fa;
47
+ --code-bg: #f0f1f3;
48
+ --code-text: #5c6166;
49
+ --code-border: var(--border-soft);
50
+ --code-pad: 8px 10px;
51
+ --card-body-pad: 4px 14px 12px;
52
+ --err-text: #e55a5a;
53
+ --red-text: #e55a5a;
54
+ --amber-text: #c77e24;
55
+ --info-blue: #22a4e6;
56
+ --link: #22a4e6;
57
+ --link-hover: #1a8fc4;
58
+ --link-soft: #22a4e6;
59
+ --md-strong: #5c6166;
60
+ --skill-blue: #22a4e6;
61
+ --plugin-purple: #b58cff;
62
+ --auth-green: #18a97c;
63
+ --scroll-thumb: #d8dade;
64
+ --scroll-thumb-hover: #b9bcc2;
65
+ --notice-err-bg: #401d23;
66
+ --notice-warn-bg: #38251f;
67
+ --notice-info-bg: #1b2544;
68
+ --notice-err-border: #ef4444;
69
+ --notice-warn-border: #f59e0b;
70
+ --notice-info-border: #3b82f6;
71
+ --send-blue: #f29718;
72
+ --send-blue-hover: #e08a0d;
73
+ --stop-red: #ef4444;
74
+ --stop-red-hover: #dc2626;
75
+ --brand-grad-a: #f29718;
76
+ --brand-grad-b: #f07171;
77
+ --on-amber: #1a1a1a;
78
+ --search-active-fg: #1a1d26;
79
+ --switch-knob: #ffffff;
80
+ --bg-elev3: rgba(255, 255, 255, 0.06);
81
+ --sunken-bg: rgba(0, 0, 0, 0.15);
82
+ --glow-015: rgba(255, 255, 255, 0.015);
83
+ --glow-025: rgba(255, 255, 255, 0.025);
84
+ --glow-03: rgba(255, 255, 255, 0.03);
85
+ --glow-04: rgba(255, 255, 255, 0.04);
86
+ --glow-05: rgba(255, 255, 255, 0.05);
87
+ --glow-12: rgba(255, 255, 255, 0.12);
88
+ --glow-18: rgba(255, 255, 255, 0.18);
89
+ --glow-22: rgba(255, 255, 255, 0.22);
90
+ --glow-38: rgba(255, 255, 255, 0.38);
91
+ --control-fg: #6b7d8f;
92
+ --control-bg: #f0f1f3;
93
+ --control-border: #e0e1e3;
94
+ --bg-image: none;
95
+ --bg-image-dim: 0.78;
96
+ --bg-image-blur: 0px;
97
+ --wallpaper-panel-alpha: 100%;
98
+ --topbar-bg: #f8f9fa;
99
+ --statusbar-bg: #fcfcfc;
100
+ --panel-bg: #f8f9fa;
101
+ --card-bg: #f8f9fa;
102
+ --chip-bg: #f0f1f3;
103
+ --msgs-bg: #fcfcfc;
104
+ --inputbox-bg: #f0f1f3;
105
+ --menu-bg: var(--bg-elev2);
106
+ }
107
+
108
+ /* ---- syntax highlighting (Ayu Light) ---- */
109
+ .hljs {
110
+ color: #5c6166;
111
+ background: #f0f1f3;
112
+ }
113
+ .hljs-keyword,
114
+ .hljs-meta .hljs-keyword {
115
+ color: #fa8532;
116
+ font-weight: 600;
117
+ }
118
+ .hljs-title,
119
+ .hljs-title.function_ {
120
+ color: #eba400;
121
+ }
122
+ .hljs-number,
123
+ .hljs-literal {
124
+ color: #a37acc;
125
+ }
126
+ .hljs-string {
127
+ color: #86b300;
128
+ }
129
+ .hljs-built_in,
130
+ .hljs-type {
131
+ color: #55b4d4;
132
+ }
133
+ .hljs-comment {
134
+ color: #6a7582;
135
+ font-style: italic;
136
+ }
@@ -18,6 +18,8 @@
18
18
  --red: #d20f39;
19
19
  --red-soft: rgba(210, 15, 57, 0.1);
20
20
  --amber: #df8e1d;
21
+ --blue: #3b82f6;
22
+ --blue-soft: rgba(59, 130, 246, 0.12);
21
23
  --term-bg: #eff1f5;
22
24
  --term-fg: #4c4f69;
23
25
  --term-cursor: #dc8a78;
@@ -68,6 +70,8 @@
68
70
  --notice-info-border: #3b82f6;
69
71
  --send-blue: #1e66f5;
70
72
  --send-blue-hover: #7287fd;
73
+ --stop-red: #ef4444;
74
+ --stop-red-hover: #dc2626;
71
75
  --brand-grad-a: #1e66f5;
72
76
  --brand-grad-b: #8839ef;
73
77
  --on-amber: #1a1a1a;