pi-web-ui 0.96.1 → 0.97.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 (41) hide show
  1. package/CHANGELOG.md +48 -2
  2. package/README.md +26 -0
  3. package/README.zh-CN.md +25 -0
  4. package/bin/pi-web-ui.mjs +11 -2
  5. package/dist/server/agent-service.js +1195 -120
  6. package/dist/server/client-state.js +89 -1
  7. package/dist/server/compacted-history.js +146 -0
  8. package/dist/server/delegate-mode.js +149 -0
  9. package/dist/server/delegate-task.js +10 -10
  10. package/dist/server/dsh/dsh-agent-service.js +35 -2
  11. package/dist/server/dsh/dsh-client.js +3 -1
  12. package/dist/server/eval-tool.js +3 -1
  13. package/dist/server/goal-review-gate.js +263 -0
  14. package/dist/server/goal-service.js +1225 -445
  15. package/dist/server/index.js +35 -1
  16. package/dist/server/lsp-tool.js +13 -10
  17. package/dist/server/markers/builtins/todo.js +0 -2
  18. package/dist/server/mcp-bridge.js +13 -0
  19. package/dist/server/patch-tool.js +1 -0
  20. package/dist/server/patch-turn-end-boundary.js +77 -0
  21. package/dist/server/permission-preset.js +44 -0
  22. package/dist/server/plan-mode.js +376 -0
  23. package/dist/server/plugin-facilities.js +36 -3
  24. package/dist/server/plugins.js +6 -3
  25. package/dist/server/protocol-version.js +1 -1
  26. package/dist/server/serialize.js +11 -0
  27. package/dist/server/settings-service.js +18 -6
  28. package/dist/server/subagents.js +360 -408
  29. package/dist/server/text-sniff.js +15 -9
  30. package/dist/server/tool-manager.js +48 -4
  31. package/dist/server/update-check.js +11 -3
  32. package/dist/server/wait-subscription-scan.js +5 -1
  33. package/package.json +3 -3
  34. package/themes/zhupi-dark.css +5 -30
  35. package/themes/zhupi.css +5 -30
  36. package/web/dist/assets/{TerminalPanel-O3GyFIGG.js → TerminalPanel-CITBqQrb.js} +1 -1
  37. package/web/dist/assets/index-DwToli0J.css +1 -0
  38. package/web/dist/assets/index-oI7UKOqJ.js +368 -0
  39. package/web/dist/index.html +2 -2
  40. package/web/dist/assets/index-B7Zp23Vc.css +0 -1
  41. package/web/dist/assets/index-ByfoaDwp.js +0 -366
@@ -16,6 +16,9 @@
16
16
  * all share one conversation list per project.
17
17
  * PI_CODING_AGENT_DIR pi config dir (auth/models/skills) — passed to the SDK
18
18
  */
19
+ // MUST be imported before the SDK is loaded so on-disk patches apply cleanly.
20
+ import "./patch-remote-catalog.js";
21
+ import "./patch-turn-end-boundary.js";
19
22
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
20
23
  import { stat } from "node:fs/promises";
21
24
  import { createServer, request as proxyRequest } from "node:http";
@@ -1633,9 +1636,20 @@ wss.on("connection", (ws) => {
1633
1636
  return;
1634
1637
  }
1635
1638
  switch (msg.type) {
1636
- case "prompt":
1639
+ case "prompt": {
1640
+ const hasAttach = Boolean(msg.attachments && msg.attachments.length > 0);
1641
+ if (!msg.text?.trim() && !hasAttach) {
1642
+ send({
1643
+ type: "notice",
1644
+ level: "warning",
1645
+ text: "发送已忽略:提示词为空且未附带文件或上下文引用。",
1646
+ textEn: "Prompt ignored: text is empty and no attachments were provided.",
1647
+ });
1648
+ break;
1649
+ }
1637
1650
  void cs.prompt(msg.text, msg.attachments, msg.queue);
1638
1651
  break;
1652
+ }
1639
1653
  case "queue_remove":
1640
1654
  cs.removeQueued(msg.kind, msg.text, msg.index);
1641
1655
  break;
@@ -1697,6 +1711,9 @@ wss.on("connection", (ws) => {
1697
1711
  send({ type: "tool_info", name: msg.name, found: false, unsupported: true });
1698
1712
  }
1699
1713
  break;
1714
+ case "get_compacted_messages":
1715
+ void cs.getCompactedMessages?.(msg.compactionMessageId, msg.conversationId);
1716
+ break;
1700
1717
  case "list_sessions":
1701
1718
  void cs.refreshSessions();
1702
1719
  break;
@@ -1723,6 +1740,11 @@ wss.on("connection", (ws) => {
1723
1740
  void cs.persistConversation(msg.id);
1724
1741
  }
1725
1742
  break;
1743
+ case "pin_conversation":
1744
+ if (typeof cs.setConversationPinned === "function") {
1745
+ void cs.setConversationPinned(msg.id, msg.pinned);
1746
+ }
1747
+ break;
1726
1748
  case "dismiss_finished_subagents":
1727
1749
  void cs.dismissFinishedSubagents(msg.parentId);
1728
1750
  break;
@@ -2019,6 +2041,7 @@ wss.on("connection", (ws) => {
2019
2041
  reviewModel: msg.reviewModel,
2020
2042
  maxRounds: msg.maxRounds,
2021
2043
  locked: msg.locked,
2044
+ execModel: msg.execModel,
2022
2045
  });
2023
2046
  break;
2024
2047
  case "clear_goal":
@@ -2036,6 +2059,7 @@ wss.on("connection", (ws) => {
2036
2059
  reviewModel: msg.reviewModel,
2037
2060
  maxRounds: msg.maxRounds,
2038
2061
  locked: msg.locked,
2062
+ execModel: msg.execModel,
2039
2063
  });
2040
2064
  break;
2041
2065
  case "get_settings":
@@ -2448,6 +2472,16 @@ wss.on("connection", (ws) => {
2448
2472
  case "plan_update":
2449
2473
  cs.updatePlan?.(msg.steps, msg.activeStepId, msg.conversationId);
2450
2474
  break;
2475
+ case "set_plan_mode":
2476
+ // 计划模式(只规划不实施):会话级开关,热生效(提示词重建 + 工具硬闸门)。
2477
+ void cs.setPlanMode?.(msg.enabled, msg.conversationId);
2478
+ break;
2479
+ case "set_delegate_mode":
2480
+ // 审查者模式(自动委派):会话级开关,默认关。开启后本对话只审阅,
2481
+ // 用户 prompt 由服务端转给常驻落盘执行对话。DSH 引擎的实现在
2482
+ // setDelegateMode 里直接拒(无 customTools 注册面 → 闸门无处可挂)。
2483
+ void cs.setDelegateMode?.(msg.enabled, msg.conversationId);
2484
+ break;
2451
2485
  case "page_response":
2452
2486
  // 浏览器(page-picker 扩展经前端)对 browser_page 的回包:恢复挂起的
2453
2487
  // pageCall;id 不匹配(超时后迟到/页面刷新)由 resolvePageCall 静默忽略。
@@ -795,6 +795,7 @@ export function makeLspTool(options) {
795
795
  const cwd = options.cwd;
796
796
  return defineTool({
797
797
  name: LSP_TOOL_NAME,
798
+ promptSnippet: "IDE-grade semantic analysis (LSP) across the workspace: definition, references, hover, diagnostics, symbols, impact check",
798
799
  label: "LSP code intelligence",
799
800
  description: `IDE-grade semantic analysis (LSP) across the workspace. Actions:
800
801
  - \`definition\`: definition of the symbol at \`line\`/\`character\` in \`path\` (file, line, snippet).
@@ -807,16 +808,18 @@ export function makeLspTool(options) {
807
808
  - \`cascade\`: impact check for \`path\` — report diagnostics of files referencing it.
808
809
  Lines are 1-indexed.`,
809
810
  parameters: Type.Object({
810
- action: Type.Union([
811
- Type.Literal("definition"),
812
- Type.Literal("references"),
813
- Type.Literal("hover"),
814
- Type.Literal("diagnostics"),
815
- Type.Literal("documentSymbol"),
816
- Type.Literal("read_symbol"),
817
- Type.Literal("workspaceSymbol"),
818
- Type.Literal("cascade"),
819
- ], {
811
+ action: Type.Unsafe({
812
+ type: "string",
813
+ enum: [
814
+ "definition",
815
+ "references",
816
+ "hover",
817
+ "diagnostics",
818
+ "documentSymbol",
819
+ "read_symbol",
820
+ "workspaceSymbol",
821
+ "cascade",
822
+ ],
820
823
  description: "The LSP operation to perform.",
821
824
  }),
822
825
  path: Type.Optional(Type.String({
@@ -40,14 +40,12 @@ export function describeTodos(state, includeDeleted = false, lang = "en") {
40
40
  .join("\n");
41
41
  }
42
42
  const TODO_GUIDANCE_ZH = [
43
- "# 内联标记工具(状态类操作请写在回答正文,不要调用工具)",
44
43
  "- 标记语法:[[todo:new:<主题>]] 新建;[[todo:set:<id>,completed|in_progress|pending]] 状态;[[todo:remove:<id>]] 删除;[[todo:dep:<id>,blocks=<依赖id,逗号分隔>]] 设依赖。",
45
44
  "- 状态变化全部用上面的 [[todo:...]] 内联标记表达,不会中断回答,无需等待返回。",
46
45
  "- 想查看/list 当前任务列表时,才用 `todo_list` 工具(读操作走工具)。",
47
46
  "- 不要编造不存在的任务 id;id 由 [[todo:new:...]] 分配,首次分配是自增整数。",
48
47
  ];
49
48
  const TODO_GUIDANCE_EN = [
50
- "# Inline marker tools (express state changes inline in your reply text — never call a tool for them)",
51
49
  "- Marker syntax: [[todo:new:<subject>]] to create; [[todo:set:<id>,completed|in_progress|pending]] for status; [[todo:remove:<id>]] to delete; [[todo:dep:<id>,blocks=<dep ids, comma-separated>]] to set dependencies.",
52
50
  "- Express all status changes with the [[todo:...]] inline markers above; they never interrupt your reply and need no waiting for a result.",
53
51
  "- Only use the `todo_list` tool (the read path goes through the tool) when you want to list the current tasks.",
@@ -393,12 +393,25 @@ function adaptMcpTool(serverName, mcpTool, client) {
393
393
  name,
394
394
  label: `${serverName} · ${mcpTool.name}`,
395
395
  description: mcpTool.description ?? `Tool ${mcpTool.name} provided by MCP server "${serverName}"`,
396
+ // 无 snippet 的工具不会进系统提示词的「Available tools」清单(SDK 只列有
397
+ // snippet 的工具),MCP 工具会变成 schema-only:模型读清单以为自己没有
398
+ // 这个能力(#403)。这里从 description 提取首句做极简 snippet。
399
+ promptSnippet: firstSentenceSnippet(mcpTool.description) ?? `MCP tool ${mcpTool.name} (server: ${serverName})`,
396
400
  parameters: mcpTool.inputSchema ?? {},
397
401
  execute: async (_toolCallId, params, _signal) => {
398
402
  return client.call(mcpTool.name, params ?? {});
399
403
  },
400
404
  };
401
405
  }
406
+ /** 从 MCP 工具 description 提取首句做 promptSnippet(≤100 字符);空描述返回 undefined。 */
407
+ function firstSentenceSnippet(description) {
408
+ const text = (description ?? "").replace(/\s+/g, " ").trim();
409
+ if (!text)
410
+ return undefined;
411
+ const cut = text.search(/[.。!?!?]/);
412
+ const head = cut > 0 ? text.slice(0, cut + 1) : text;
413
+ return head.length > 100 ? `${head.slice(0, 99)}…` : head;
414
+ }
402
415
  /** 工具名必须是 [A-Za-z0-9_-]+(与插件工具同规则),MCP 可能含冒号/斜杠 — 归一化。 */
403
416
  function sanitizeToolName(name) {
404
417
  const cleaned = (name || "").replace(/[^A-Za-z0-9_-]/g, "_");
@@ -18,6 +18,7 @@ export function makePatchTool(options) {
18
18
  const cwd = options.cwd;
19
19
  return defineTool({
20
20
  name: PATCH_TOOL_NAME,
21
+ promptSnippet: "apply content-hashed, line-anchored patches to workspace files (prevents stale edits, line drift, and indentation hallucination)",
21
22
  label: "Apply Hashline patch",
22
23
  description: `Apply content-hashed, line-anchored patches to workspace files — prevents stale edits, line drift, and indentation hallucination.
23
24
  Each file section starts with \`[path#TAG]\` (or \`[path]\` if the hash is unknown yet). Operations:
@@ -0,0 +1,77 @@
1
+ /**
2
+ * turn_end boundary error patch for @earendil-works/pi-coding-agent (issue #411).
3
+ *
4
+ * In SDK 0.87+, when an in-flight turn is aborted or torn down before its assistant
5
+ * message is persisted, `_dispatchTurnEndBoundary()` in `agent-session.js` calls
6
+ * `this._extensionRunner.emitError({ event: "turn_end", error: "turn_end could not resolve..." })`
7
+ * right before returning `false`, causing false-positive extension errors to flood
8
+ * both the main conversation and subagents.
9
+ *
10
+ * This module idempotently patches `agent-session.js` in the installed SDK to
11
+ * silently skip the boundary (`return false`) without emitting an error, matching
12
+ * the upstream issue expectation (earendil-works/pi#10149).
13
+ *
14
+ * Same pattern as patch-remote-catalog.ts and patch-node-pty.ts: best-effort, idempotent,
15
+ * and skips gracefully if the SDK code structure differs or is already fixed upstream.
16
+ */
17
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { dirname, join } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { sdkCopies } from "./sdk-origin.js";
21
+ export const TARGET_PATTERN = /if\s*\(!messageEntryId\)\s*\{\s*this\._extensionRunner\.emitError\(\{\s*extensionPath:\s*"<boundary>",\s*event:\s*"turn_end",\s*error:\s*"turn_end could not resolve the persisted assistant entry ID",\s*\}\);\s*return false;\s*\}/;
22
+ export const PATCHED_SENTINEL = "// pi-web-ui patch (issue #411: silent turn_end on aborted turns)";
23
+ export const REPLACEMENT = `if (!messageEntryId) {
24
+ ${PATCHED_SENTINEL}
25
+ return false;
26
+ }`;
27
+ /**
28
+ * 纯函数:给定 agent-session.js 内容,返回打补丁后的内容。
29
+ * - 命中目标代码段 → 替换并返回新内容;
30
+ * - 已打过补丁(含 PATCHED_SENTINEL)→ 原样返回;
31
+ * - 目标代码段未命中(上游已修复或版本不匹配)→ 原样返回,静默跳过。
32
+ */
33
+ export function patchAgentSessionSource(src) {
34
+ if (src.includes(PATCHED_SENTINEL)) {
35
+ return { patched: false, code: src };
36
+ }
37
+ if (!TARGET_PATTERN.test(src)) {
38
+ return { patched: false, code: src };
39
+ }
40
+ return { patched: true, code: src.replace(TARGET_PATTERN, REPLACEMENT) };
41
+ }
42
+ export function candidateAgentSessionFiles() {
43
+ const files = new Set();
44
+ try {
45
+ const entry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
46
+ files.add(join(dirname(entry), "core", "agent-session.js"));
47
+ }
48
+ catch {
49
+ // Ignore resolve failures
50
+ }
51
+ try {
52
+ for (const copy of sdkCopies()) {
53
+ files.add(join(dirname(copy.path), "dist", "core", "agent-session.js"));
54
+ }
55
+ }
56
+ catch {
57
+ // Ignore copy inspection failures
58
+ }
59
+ return [...files];
60
+ }
61
+ export function applyPatch() {
62
+ for (const file of candidateAgentSessionFiles()) {
63
+ try {
64
+ if (!existsSync(file))
65
+ continue;
66
+ const src = readFileSync(file, "utf8");
67
+ const { patched, code } = patchAgentSessionSource(src);
68
+ if (patched) {
69
+ writeFileSync(file, code, "utf8");
70
+ }
71
+ }
72
+ catch {
73
+ // best-effort
74
+ }
75
+ }
76
+ }
77
+ applyPatch();
@@ -1,6 +1,50 @@
1
1
  /**
2
2
  * 权限预设相关辅助逻辑与会话回放。
3
3
  */
4
+ /**
5
+ * 从会话 sessionManager entries 回放恢复最后设定的**计划模式**状态(同 permission/preset
6
+ * 的落盘口径:切会话/重载不丢)。从未设定或非布尔 → false。
7
+ */
8
+ export function readPlanModeFromSession(sm) {
9
+ try {
10
+ const mgr = sm;
11
+ if (typeof mgr?.getEntries !== "function")
12
+ return false;
13
+ const entries = mgr.getEntries();
14
+ for (let i = entries.length - 1; i >= 0; i--) {
15
+ const e = entries[i];
16
+ if (e?.customType === "plan/mode" && typeof e.data?.enabled === "boolean")
17
+ return e.data.enabled;
18
+ }
19
+ }
20
+ catch {
21
+ // ignore
22
+ }
23
+ return false;
24
+ }
25
+ /**
26
+ * 从会话 sessionManager entries 回放恢复最后设定的**审查者模式**状态(同 plan/mode
27
+ * 口径:customType "delegate/mode")。从未设定或非布尔 → false。
28
+ * ⚠️ 执行对话 id **不回放**:常驻执行对话是本进程内的活对象,重启后
29
+ * 首条请求会重新建一个(宁可多一个执行对话,也别指向一个不存在的 id)。
30
+ */
31
+ export function readDelegateModeFromSession(sm) {
32
+ try {
33
+ const mgr = sm;
34
+ if (typeof mgr?.getEntries !== "function")
35
+ return false;
36
+ const entries = mgr.getEntries();
37
+ for (let i = entries.length - 1; i >= 0; i--) {
38
+ const e = entries[i];
39
+ if (e?.customType === "delegate/mode" && typeof e.data?.enabled === "boolean")
40
+ return e.data.enabled;
41
+ }
42
+ }
43
+ catch {
44
+ // ignore
45
+ }
46
+ return false;
47
+ }
4
48
  /**
5
49
  * 从会话 sessionManager entries 回放恢复最后设定的权限预设(若从未设定则返回 undefined)。
6
50
  * customType 为 "permission/preset",载荷结构为 { preset: string }。