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.
- package/CHANGELOG.md +48 -2
- package/README.md +26 -0
- package/README.zh-CN.md +25 -0
- package/bin/pi-web-ui.mjs +11 -2
- package/dist/server/agent-service.js +1195 -120
- package/dist/server/client-state.js +89 -1
- package/dist/server/compacted-history.js +146 -0
- package/dist/server/delegate-mode.js +149 -0
- package/dist/server/delegate-task.js +10 -10
- package/dist/server/dsh/dsh-agent-service.js +35 -2
- package/dist/server/dsh/dsh-client.js +3 -1
- package/dist/server/eval-tool.js +3 -1
- package/dist/server/goal-review-gate.js +263 -0
- package/dist/server/goal-service.js +1225 -445
- package/dist/server/index.js +35 -1
- package/dist/server/lsp-tool.js +13 -10
- package/dist/server/markers/builtins/todo.js +0 -2
- package/dist/server/mcp-bridge.js +13 -0
- package/dist/server/patch-tool.js +1 -0
- package/dist/server/patch-turn-end-boundary.js +77 -0
- package/dist/server/permission-preset.js +44 -0
- package/dist/server/plan-mode.js +376 -0
- package/dist/server/plugin-facilities.js +36 -3
- package/dist/server/plugins.js +6 -3
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/serialize.js +11 -0
- package/dist/server/settings-service.js +18 -6
- package/dist/server/subagents.js +360 -408
- package/dist/server/text-sniff.js +15 -9
- package/dist/server/tool-manager.js +48 -4
- package/dist/server/update-check.js +11 -3
- package/dist/server/wait-subscription-scan.js +5 -1
- package/package.json +3 -3
- package/themes/zhupi-dark.css +5 -30
- package/themes/zhupi.css +5 -30
- package/web/dist/assets/{TerminalPanel-O3GyFIGG.js → TerminalPanel-CITBqQrb.js} +1 -1
- package/web/dist/assets/index-DwToli0J.css +1 -0
- package/web/dist/assets/index-oI7UKOqJ.js +368 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-B7Zp23Vc.css +0 -1
- package/web/dist/assets/index-ByfoaDwp.js +0 -366
package/dist/server/index.js
CHANGED
|
@@ -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 静默忽略。
|
package/dist/server/lsp-tool.js
CHANGED
|
@@ -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.
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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 }。
|