pi-web-ui 0.96.1 → 0.98.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +144 -2
- package/README.md +29 -3
- package/README.zh-CN.md +28 -3
- package/bin/pi-web-ui.mjs +75 -25
- package/dist/server/agent-service.js +1829 -262
- package/dist/server/approval-rules.js +5 -10
- package/dist/server/atomic-file.js +40 -0
- package/dist/server/attachments.js +22 -0
- package/dist/server/claim-store.js +5 -11
- package/dist/server/client-state.js +296 -11
- package/dist/server/compact-context-tool.js +3 -1
- package/dist/server/compacted-history.js +148 -0
- package/dist/server/composer-drafts.js +4 -6
- package/dist/server/control-socket.js +0 -40
- package/dist/server/delegate-mode.js +143 -0
- package/dist/server/delegate-task.js +10 -10
- package/dist/server/dsh/dsh-agent-service.js +66 -31
- package/dist/server/dsh/dsh-client.js +48 -10
- package/dist/server/dsh/dsh-serialize.js +12 -10
- package/dist/server/dsh/dsh-sessions.js +20 -6
- package/dist/server/dsh/runtime/goal-rpc.mjs +14 -1
- package/dist/server/dsh/runtime/runtime-root.mjs +1 -1
- package/dist/server/ensure-bash.js +3 -5
- package/dist/server/eval-tool.js +11 -5
- package/dist/server/goal-review-gate.js +250 -0
- package/dist/server/goal-service.js +1272 -448
- package/dist/server/index.js +118 -16
- package/dist/server/launch-origin.js +0 -4
- package/dist/server/locales.js +3 -5
- package/dist/server/lsp-tool.js +123 -24
- package/dist/server/markers/builtins/notify.js +4 -2
- package/dist/server/markers/builtins/rename.js +0 -1
- package/dist/server/markers/builtins/todo.js +6 -28
- package/dist/server/markers/marker.js +0 -15
- package/dist/server/markers/registry.js +0 -3
- package/dist/server/markers/store.js +0 -3
- package/dist/server/mcp-bridge.js +13 -0
- package/dist/server/mcp-hot-reload.js +12 -5
- package/dist/server/model-admin.js +19 -18
- package/dist/server/model-enrich.js +0 -3
- package/dist/server/patch-tool.js +22 -6
- package/dist/server/patch-turn-end-boundary.js +77 -0
- package/dist/server/permission-preset.js +69 -0
- package/dist/server/plan-manager.js +150 -5
- package/dist/server/plan-mode.js +414 -0
- package/dist/server/plugin-api-catalog.js +2 -0
- package/dist/server/plugin-catalog.js +6 -6
- package/dist/server/plugin-dom.js +3 -4
- package/dist/server/plugin-facilities.js +39 -8
- package/dist/server/plugin-grants.js +5 -8
- package/dist/server/plugin-permissions.js +4 -6
- package/dist/server/plugin-schedule.js +4 -7
- package/dist/server/plugin-updater.js +151 -38
- package/dist/server/plugins.js +22 -5
- package/dist/server/prompt-composer.js +12 -3
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/resolve-global-sdk.js +8 -0
- package/dist/server/schedule-agent-tool.js +5 -1
- package/dist/server/scheduler-tasks.js +46 -27
- package/dist/server/sdk-origin.js +27 -0
- package/dist/server/serialize.js +33 -9
- package/dist/server/settings-service.js +18 -6
- package/dist/server/slash-commands.js +16 -0
- package/dist/server/subagent-templates.js +4 -10
- package/dist/server/subagents.js +379 -416
- package/dist/server/terminals.js +11 -6
- package/dist/server/text-quote.js +59 -0
- package/dist/server/text-sniff.js +15 -9
- package/dist/server/tool-manager.js +60 -7
- package/dist/server/tool-overrides.js +24 -0
- package/dist/server/update-check.js +80 -28
- package/dist/server/wait-subscription-scan.js +5 -1
- package/extensions/webui.ts +15 -1
- package/package.json +33 -21
- package/plugin-sdk/client-utils.mjs +92 -0
- package/plugin-sdk/index.d.ts +21 -0
- package/plugin-sdk/index.mjs +2 -0
- package/plugins/catalog.json +7 -0
- package/themes/zhupi-dark.css +5 -30
- package/themes/zhupi.css +5 -30
- package/web/dist/assets/{TerminalPanel-O3GyFIGG.js → TerminalPanel-C88r3r3Q.js} +1 -1
- package/web/dist/assets/index-2jDtjHvW.css +1 -0
- package/web/dist/assets/index-BjdK7s1M.js +368 -0
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +2 -1
- package/web/public/sw.js +2 -1
- package/web/dist/assets/index-B7Zp23Vc.css +0 -1
- package/web/dist/assets/index-ByfoaDwp.js +0 -366
|
@@ -15,39 +15,13 @@ function parseId(raw) {
|
|
|
15
15
|
const n = Number(raw);
|
|
16
16
|
return Number.isInteger(n) && n > 0 ? n : null;
|
|
17
17
|
}
|
|
18
|
-
function formatStatus(s) {
|
|
19
|
-
switch (s) {
|
|
20
|
-
case "pending":
|
|
21
|
-
return "pending";
|
|
22
|
-
case "in_progress":
|
|
23
|
-
return "in_progress";
|
|
24
|
-
case "completed":
|
|
25
|
-
return "completed";
|
|
26
|
-
default:
|
|
27
|
-
return s;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
export function describeTodos(state, includeDeleted = false, lang = "en") {
|
|
31
|
-
const visible = state.tasks.filter((t) => includeDeleted || t.status !== "deleted");
|
|
32
|
-
if (visible.length === 0)
|
|
33
|
-
return pick(lang, "[todo] (空)", "[todo] (empty)", "markers.todo.list.empty");
|
|
34
|
-
return visible
|
|
35
|
-
.map((t) => {
|
|
36
|
-
const form = t.status === "in_progress" && t.activeForm ? ` (${t.activeForm})` : "";
|
|
37
|
-
const deps = t.blockedBy.length ? ` ⛓ ${t.blockedBy.join(",")}` : "";
|
|
38
|
-
return `[${formatStatus(t.status)}] #${t.id} ${t.subject}${form}${deps}`;
|
|
39
|
-
})
|
|
40
|
-
.join("\n");
|
|
41
|
-
}
|
|
42
18
|
const TODO_GUIDANCE_ZH = [
|
|
43
|
-
"# 内联标记工具(状态类操作请写在回答正文,不要调用工具)",
|
|
44
19
|
"- 标记语法:[[todo:new:<主题>]] 新建;[[todo:set:<id>,completed|in_progress|pending]] 状态;[[todo:remove:<id>]] 删除;[[todo:dep:<id>,blocks=<依赖id,逗号分隔>]] 设依赖。",
|
|
45
20
|
"- 状态变化全部用上面的 [[todo:...]] 内联标记表达,不会中断回答,无需等待返回。",
|
|
46
21
|
"- 想查看/list 当前任务列表时,才用 `todo_list` 工具(读操作走工具)。",
|
|
47
22
|
"- 不要编造不存在的任务 id;id 由 [[todo:new:...]] 分配,首次分配是自增整数。",
|
|
48
23
|
];
|
|
49
24
|
const TODO_GUIDANCE_EN = [
|
|
50
|
-
"# Inline marker tools (express state changes inline in your reply text — never call a tool for them)",
|
|
51
25
|
"- 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
26
|
"- Express all status changes with the [[todo:...]] inline markers above; they never interrupt your reply and need no waiting for a result.",
|
|
53
27
|
"- Only use the `todo_list` tool (the read path goes through the tool) when you want to list the current tasks.",
|
|
@@ -66,7 +40,9 @@ export const todoMarker = {
|
|
|
66
40
|
const op = token.op;
|
|
67
41
|
switch (op) {
|
|
68
42
|
case "new": {
|
|
69
|
-
|
|
43
|
+
// #499:subject 是唯一「整段自由文本」参数——args.join 还原含逗号的主题,
|
|
44
|
+
// 否则 `[[todo:new:修复登录页,同步改注册页]]` 的后半段被静默丢弃。
|
|
45
|
+
const subject = token.args.join(",").trim();
|
|
70
46
|
if (!subject)
|
|
71
47
|
return {
|
|
72
48
|
applied: false,
|
|
@@ -181,7 +157,9 @@ export const todoMarker = {
|
|
|
181
157
|
const depsText = deps.length ? deps.join(",") : lang === "zh" ? "(无)" : "(none)";
|
|
182
158
|
return {
|
|
183
159
|
applied: true,
|
|
184
|
-
feedback: pick(lang, `#${id} 依赖:${depsText}`,
|
|
160
|
+
feedback: pick(lang, `#${id} 依赖:${depsText}`,
|
|
161
|
+
// #499:实现是 blockedBy(deps 先完成)——原英文 "blocks" 把依赖方向说反了。
|
|
162
|
+
`#${id} is blocked by: ${depsText}`, "markers.todo.dep.blocks.updated", { id: id, depsText: depsText }),
|
|
185
163
|
};
|
|
186
164
|
}
|
|
187
165
|
default:
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
* marker.ts — 通用内联标记核心抽象(内置版)。
|
|
3
3
|
* 复刻自 pi-marker-tools,保持相同解析语义,便于 AI 无缝迁移。
|
|
4
4
|
*/
|
|
5
|
-
export const MARKER_OPEN = "[[";
|
|
6
|
-
export const MARKER_CLOSE = "]]";
|
|
7
5
|
// ---------------------------------------------------------------------------
|
|
8
6
|
// 解析器
|
|
9
7
|
// ---------------------------------------------------------------------------
|
|
@@ -38,16 +36,3 @@ export function parseMarkers(text) {
|
|
|
38
36
|
}
|
|
39
37
|
return tokens;
|
|
40
38
|
}
|
|
41
|
-
export function stripMarkers(text) {
|
|
42
|
-
return text.replace(TOKEN_RE, () => "");
|
|
43
|
-
}
|
|
44
|
-
export function replaceToken(text, raw, replacement) {
|
|
45
|
-
return text.split(raw).join(replacement);
|
|
46
|
-
}
|
|
47
|
-
export function serializeToken(token) {
|
|
48
|
-
const parts = [token.tool, token.op, ...token.args];
|
|
49
|
-
const kwargs = Object.entries(token.kwargs)
|
|
50
|
-
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
51
|
-
.map(([k, v]) => `${k}=${v}`);
|
|
52
|
-
return `${MARKER_OPEN}${[...parts, ...kwargs].join(":")}${MARKER_CLOSE}`;
|
|
53
|
-
}
|
|
@@ -11,9 +11,6 @@ export function getMarker(name) {
|
|
|
11
11
|
export function allMarkers() {
|
|
12
12
|
return [...registry.values()];
|
|
13
13
|
}
|
|
14
|
-
export function lookupToken(name) {
|
|
15
|
-
return registry.get(name);
|
|
16
|
-
}
|
|
17
14
|
export function collectGuidance(disabled = new Set(), lang = "en") {
|
|
18
15
|
const out = [];
|
|
19
16
|
for (const m of allMarkers()) {
|
|
@@ -23,9 +23,6 @@ export function loadStateFromBranch(branch, namespace) {
|
|
|
23
23
|
}
|
|
24
24
|
return latest?.state;
|
|
25
25
|
}
|
|
26
|
-
export function hasStateInBranch(branch, namespace) {
|
|
27
|
-
return loadStateFromBranch(branch, namespace) !== undefined;
|
|
28
|
-
}
|
|
29
26
|
/** 追加快照:优先走 sessionManager.appendCustomEntry,否则回退到回调。 */
|
|
30
27
|
export function appendSnapshot(mgr, namespace, state, fallbackSave) {
|
|
31
28
|
const snapshot = {
|
|
@@ -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, "_");
|
|
@@ -31,15 +31,22 @@ export function createMcpHotReload(deps) {
|
|
|
31
31
|
let debounce = null;
|
|
32
32
|
let poller = null;
|
|
33
33
|
let watcher = null;
|
|
34
|
-
/** 读一次磁盘:指纹 + 清单(`servers` 为 null =
|
|
34
|
+
/** 读一次磁盘:指纹 + 清单(`servers` 为 null = 坏配置/读失败)。文件不在按「没有服务器」算。 */
|
|
35
35
|
function readOnce() {
|
|
36
36
|
let raw;
|
|
37
37
|
try {
|
|
38
38
|
raw = readFileSync(file, "utf8");
|
|
39
39
|
}
|
|
40
|
-
catch {
|
|
41
|
-
//
|
|
42
|
-
|
|
40
|
+
catch (err) {
|
|
41
|
+
// 不变量 3 只覆盖「文件不在」:删掉 mcp.json = 关掉全部 MCP 服务器。
|
|
42
|
+
// #497:EBUSY/EACCES 等临时读失败(OneDrive/杀毒独占、权限误改)既不是
|
|
43
|
+
// 坏配置也不是删除意图——按坏配置口径走(servers: null → 保留在跑的
|
|
44
|
+
// 服务器 + 一次性提示),否则会把在跑的 MCP 服务器全杀还报「成功 0 个」。
|
|
45
|
+
const code = err?.code;
|
|
46
|
+
if (code === "ENOENT")
|
|
47
|
+
return { fp: "empty", servers: {} };
|
|
48
|
+
log(`[mcp] mcp.json 读取失败(${code ?? String(err)}),按坏配置处理:保留在跑的 MCP 服务器`);
|
|
49
|
+
return { fp: `unreadable:${code ?? "unknown"}`, servers: null };
|
|
43
50
|
}
|
|
44
51
|
const parsed = parseMcpConfig(raw);
|
|
45
52
|
if (!parsed)
|
|
@@ -54,7 +61,7 @@ export function createMcpHotReload(deps) {
|
|
|
54
61
|
applied = fp;
|
|
55
62
|
if (!servers) {
|
|
56
63
|
log("[mcp] mcp.json 解析失败,保留在跑的 MCP 服务器");
|
|
57
|
-
deps.onNotice?.("warning", "mcp.json
|
|
64
|
+
deps.onNotice?.("warning", "mcp.json 解析/读取失败,已保留当前 MCP 服务器(恢复可读后会自动重载)", "Failed to parse/read mcp.json — keeping the running MCP servers (it reloads automatically once readable)");
|
|
58
65
|
return "invalid";
|
|
59
66
|
}
|
|
60
67
|
const summary = await deps.reload();
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* 经 ModelAdminHost 与 ClientSession 解耦(同 settings/goal/slash 服务模式)。
|
|
12
12
|
* UI 文案直接中文(服务端 notice 约定)。apiKey/headers 绝不下发浏览器。
|
|
13
13
|
*/
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, renameSync
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, renameSync } from "node:fs";
|
|
15
15
|
import { dirname, join } from "node:path";
|
|
16
|
+
import { writeJsonAtomicSync } from "./atomic-file.js";
|
|
16
17
|
import { enrichBatch } from "./model-enrich.js";
|
|
17
18
|
import { pick } from "./i18n.js";
|
|
18
19
|
import { ProviderOAuthFlowManager } from "./provider-oauth-flow.js";
|
|
@@ -65,12 +66,9 @@ export function graduateOverlayModels(providers, official) {
|
|
|
65
66
|
}
|
|
66
67
|
return graduated;
|
|
67
68
|
}
|
|
68
|
-
/** 原子写 JSON(tmp + rename
|
|
69
|
-
* 杀会把整份配置截断,下次读盘即空表。tmp 名带 pid,避免同机多进程互踩。 */
|
|
69
|
+
/** 原子写 JSON(tmp + rename):进程崩溃不留半截文件。 */
|
|
70
70
|
function atomicWriteJson(path, data) {
|
|
71
|
-
|
|
72
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
73
|
-
renameSync(tmp, path);
|
|
71
|
+
writeJsonAtomicSync(path, data);
|
|
74
72
|
}
|
|
75
73
|
/** Strip // and /* *\/ comments without touching string literals (URLs contain //). */
|
|
76
74
|
function stripJsonComments(src) {
|
|
@@ -378,13 +376,8 @@ export class ModelAdminService {
|
|
|
378
376
|
return out;
|
|
379
377
|
}
|
|
380
378
|
writeProviderKeys(data) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
//(Windows 忽略 mode 无害,POSIX 生效)。
|
|
384
|
-
const path = this.providerKeysPath();
|
|
385
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
386
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
|
|
387
|
-
renameSync(tmp, path);
|
|
379
|
+
// 原子写(tmp+rename):崩溃不留半截 JSON。
|
|
380
|
+
writeJsonAtomicSync(this.providerKeysPath(), data);
|
|
388
381
|
}
|
|
389
382
|
/** 解析失败的配置文件改名留存为 <name>.corrupt-<timestamp>。改名失败(占用/
|
|
390
383
|
* 跨盘等)也不抛——留存是尽力而为,绝不能阻塞主流程。 */
|
|
@@ -1461,7 +1454,15 @@ export class ModelAdminService {
|
|
|
1461
1454
|
}
|
|
1462
1455
|
const fetched = await ModelAdminService.probeModelsEndpoint(baseUrl, saved.apiKey, saved.authHeader === true ? true : undefined, saved.api, saved.headers, lang);
|
|
1463
1456
|
// Merge: manual values win; fetched fills blanks and appends new ids.
|
|
1464
|
-
|
|
1457
|
+
// #486:probe 是数秒级网络 await,期间用户可能已在设置面板编辑同一服务商——
|
|
1458
|
+
// 以盘上最新条目为合并基准(旧快照 saved 只提供探测参数),否则刷新会用
|
|
1459
|
+
// 旧快照整表覆盖,静默回滚并发编辑(删除的模型复活、手动新增丢失)。
|
|
1460
|
+
const fresh = this.readModelsConfig().providers[pid];
|
|
1461
|
+
if (!fresh) {
|
|
1462
|
+
// probe 期间服务商被删除:不再写回,避免用旧快照复活已删除的条目。
|
|
1463
|
+
return done(false, { error: "provider deleted during refresh" });
|
|
1464
|
+
}
|
|
1465
|
+
const prev = new Map((fresh.models ?? []).map((m) => [m.id, m]));
|
|
1465
1466
|
let added = 0;
|
|
1466
1467
|
for (const f of fetched) {
|
|
1467
1468
|
const cur = prev.get(f.id);
|
|
@@ -1478,11 +1479,11 @@ export class ModelAdminService {
|
|
|
1478
1479
|
const merged = [...prev.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
1479
1480
|
await this.saveModelConfig(pid, {
|
|
1480
1481
|
providerId: pid,
|
|
1481
|
-
name:
|
|
1482
|
-
api:
|
|
1483
|
-
baseUrl:
|
|
1482
|
+
name: fresh.name,
|
|
1483
|
+
api: fresh.api,
|
|
1484
|
+
baseUrl: fresh.baseUrl,
|
|
1484
1485
|
// apiKey/headers 不回传浏览器——saveModelConfig 会保留旧值
|
|
1485
|
-
authHeader:
|
|
1486
|
+
authHeader: fresh.authHeader === true ? true : undefined,
|
|
1486
1487
|
models: merged,
|
|
1487
1488
|
});
|
|
1488
1489
|
this.host.emit({
|
|
@@ -159,9 +159,6 @@ async function fetchJson(fetchFn, url, maxBytes, timeoutMs, signal) {
|
|
|
159
159
|
/** 目录抓取缓存(24h TTL,进程内;首跑后无感)。 */
|
|
160
160
|
const catalogCache = new Map();
|
|
161
161
|
const CATALOG_TTL_MS = 24 * 3600 * 1000;
|
|
162
|
-
export function clearEnrichCache() {
|
|
163
|
-
catalogCache.clear();
|
|
164
|
-
}
|
|
165
162
|
export async function getOpenRouterCatalog(fetchFn = fetch, signal) {
|
|
166
163
|
const hit = catalogCache.get("openrouter");
|
|
167
164
|
if (hit && Date.now() - hit.at < CATALOG_TTL_MS)
|
|
@@ -11,13 +11,16 @@
|
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
12
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { Type } from "typebox";
|
|
14
|
-
import {
|
|
14
|
+
import { pick } from "./i18n.js";
|
|
15
|
+
import { applyHashlinePatch, formatHashlineHeader, globalSnapshotStore, } from "./hashline-engine.js";
|
|
15
16
|
import { getLiveLspDiagnostics } from "./lsp-tool.js";
|
|
16
17
|
export const PATCH_TOOL_NAME = "patch";
|
|
17
18
|
export function makePatchTool(options) {
|
|
18
19
|
const cwd = options.cwd;
|
|
20
|
+
const getLang = options.lang ?? (() => "en");
|
|
19
21
|
return defineTool({
|
|
20
22
|
name: PATCH_TOOL_NAME,
|
|
23
|
+
promptSnippet: "apply content-hashed, line-anchored patches to workspace files (prevents stale edits, line drift, and indentation hallucination)",
|
|
21
24
|
label: "Apply Hashline patch",
|
|
22
25
|
description: `Apply content-hashed, line-anchored patches to workspace files — prevents stale edits, line drift, and indentation hallucination.
|
|
23
26
|
Each file section starts with \`[path#TAG]\` (or \`[path]\` if the hash is unknown yet). Operations:
|
|
@@ -33,10 +36,13 @@ On divergence the engine attempts a 3-way merge. After success the tool returns
|
|
|
33
36
|
description: "The complete Hashline patch text containing one or more [path#TAG] sections with PUT/CUT/REM/MV operations.",
|
|
34
37
|
}),
|
|
35
38
|
timeout: Type.Optional(Type.Number({
|
|
36
|
-
|
|
39
|
+
minimum: 1,
|
|
40
|
+
maximum: 300,
|
|
41
|
+
description: "Optional execution timeout in seconds (1-300).",
|
|
37
42
|
})),
|
|
38
43
|
}),
|
|
39
44
|
async execute(_callId, params, _signal, _onUpdate, _ctx) {
|
|
45
|
+
const L = getLang();
|
|
40
46
|
const patchText = typeof params?.patch === "string" ? params.patch : "";
|
|
41
47
|
if (!patchText.trim()) {
|
|
42
48
|
const emptyReport = {
|
|
@@ -46,7 +52,12 @@ On divergence the engine attempts a 3-way merge. After success the tool returns
|
|
|
46
52
|
error: "Empty patch",
|
|
47
53
|
};
|
|
48
54
|
return {
|
|
49
|
-
content: [
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: pick(L, "错误:未提供任何 patch 补丁内容。", "Error: No patch content provided."),
|
|
59
|
+
},
|
|
60
|
+
],
|
|
50
61
|
details: emptyReport,
|
|
51
62
|
};
|
|
52
63
|
}
|
|
@@ -56,7 +67,12 @@ On divergence the engine attempts a 3-way merge. After success the tool returns
|
|
|
56
67
|
});
|
|
57
68
|
if (!report.ok) {
|
|
58
69
|
return {
|
|
59
|
-
content: [
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: pick(L, `补丁应用失败:\n${report.summary}`, `Patch Failed:\n${report.summary}`),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
60
76
|
details: report,
|
|
61
77
|
};
|
|
62
78
|
}
|
|
@@ -65,12 +81,12 @@ On divergence the engine attempts a 3-way merge. After success the tool returns
|
|
|
65
81
|
for (const r of report.results) {
|
|
66
82
|
if (r.op !== "deleted" && r.newHash) {
|
|
67
83
|
const targetPath = r.newPath || r.filePath;
|
|
68
|
-
textOutput.push(`\nNext edit anchor for ${targetPath}: \`${formatHashlineHeader(targetPath, r.newHash)}\``);
|
|
84
|
+
textOutput.push(pick(L, `\n${targetPath} 的下一处编辑锚点:\`${formatHashlineHeader(targetPath, r.newHash)}\``, `\nNext edit anchor for ${targetPath}: \`${formatHashlineHeader(targetPath, r.newHash)}\``));
|
|
69
85
|
try {
|
|
70
86
|
const fullPath = resolve(cwd, targetPath);
|
|
71
87
|
const diags = await getLiveLspDiagnostics(fullPath, cwd);
|
|
72
88
|
if (diags) {
|
|
73
|
-
textOutput.push(`\nLive LSP diagnostics for ${targetPath}:\n${diags}`);
|
|
89
|
+
textOutput.push(pick(L, `\n${targetPath} 的实时 LSP 诊断:\n${diags}`, `\nLive LSP diagnostics for ${targetPath}:\n${diags}`));
|
|
74
90
|
}
|
|
75
91
|
}
|
|
76
92
|
catch { }
|
|
@@ -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 }。
|
|
@@ -23,3 +67,28 @@ export function readPermissionFromSession(sm) {
|
|
|
23
67
|
}
|
|
24
68
|
return undefined;
|
|
25
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* 从会话 sessionManager entries 回放恢复最后保存的结构化计划状态。
|
|
72
|
+
* customType 为 "plan/update" 或 "pi-web-ui/plan",载荷结构为 { plan: PlanState }。
|
|
73
|
+
*/
|
|
74
|
+
export function readPlanFromSession(sm) {
|
|
75
|
+
try {
|
|
76
|
+
const mgr = sm;
|
|
77
|
+
if (typeof mgr?.getEntries !== "function")
|
|
78
|
+
return null;
|
|
79
|
+
const entries = mgr.getEntries();
|
|
80
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
81
|
+
const e = entries[i];
|
|
82
|
+
if (e?.type === "custom" &&
|
|
83
|
+
(e.customType === "plan/update" || e.customType === "pi-web-ui/plan") &&
|
|
84
|
+
e.data?.plan &&
|
|
85
|
+
Array.isArray(e.data.plan.steps)) {
|
|
86
|
+
return e.data.plan;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// ignore
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|