pi-web-ui 0.97.0 → 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 +97 -1
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/bin/pi-web-ui.mjs +64 -23
- package/dist/server/agent-service.js +637 -145
- 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 +220 -23
- package/dist/server/compact-context-tool.js +3 -1
- package/dist/server/compacted-history.js +7 -5
- package/dist/server/composer-drafts.js +4 -6
- package/dist/server/control-socket.js +0 -40
- package/dist/server/delegate-mode.js +6 -12
- package/dist/server/dsh/dsh-agent-service.js +32 -30
- package/dist/server/dsh/dsh-client.js +45 -9
- 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 +8 -4
- package/dist/server/goal-review-gate.js +7 -20
- package/dist/server/goal-service.js +49 -5
- package/dist/server/index.js +83 -15
- package/dist/server/launch-origin.js +0 -4
- package/dist/server/locales.js +3 -5
- package/dist/server/lsp-tool.js +110 -14
- 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 -26
- 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-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 +21 -6
- package/dist/server/permission-preset.js +25 -0
- package/dist/server/plan-manager.js +150 -5
- package/dist/server/plan-mode.js +58 -20
- 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 +3 -5
- 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 +16 -2
- 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 +22 -9
- package/dist/server/slash-commands.js +16 -0
- package/dist/server/subagent-templates.js +4 -10
- package/dist/server/subagents.js +22 -11
- package/dist/server/terminals.js +11 -6
- package/dist/server/text-quote.js +59 -0
- package/dist/server/tool-manager.js +12 -3
- package/dist/server/tool-overrides.js +24 -0
- package/dist/server/update-check.js +69 -25
- package/extensions/webui.ts +15 -1
- package/package.json +31 -19
- 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/web/dist/assets/{TerminalPanel-CITBqQrb.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-DwToli0J.css +0 -1
- package/web/dist/assets/index-oI7UKOqJ.js +0 -368
package/dist/server/serialize.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
const
|
|
1
|
+
import { splitQuotedPrompt } from "./text-quote.js";
|
|
2
|
+
export const TEXT_CAP = 200_000;
|
|
3
|
+
export const TOOL_OUTPUT_CAP = 100_000;
|
|
4
|
+
export const ARGS_CAP = 20_000;
|
|
4
5
|
/**
|
|
5
6
|
* toolResult 里单张图片进快照的上限(dataUrl 字符数 ≈ 1.5MB 二进制)。
|
|
6
7
|
* 视口/元素截图随便过;超大整页截图回落成占位文本(模型侧不受影响 —— 图它
|
|
@@ -17,10 +18,11 @@ const TOOL_RESULT_IMAGE_MAX = 8;
|
|
|
17
18
|
* 前端还得写容错);工具作者应自己封顶(见 present-files-tool.ts 的摘录预算)。
|
|
18
19
|
*/
|
|
19
20
|
const TOOL_DETAILS_CAP = 64_000;
|
|
20
|
-
function truncate(s, cap) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
export function truncate(s, cap) {
|
|
22
|
+
const str = typeof s === "string" ? s : String(s ?? "");
|
|
23
|
+
if (str.length <= cap)
|
|
24
|
+
return { text: str, truncated: false };
|
|
25
|
+
return { text: `${str.slice(0, cap)}\n\n… [truncated]`, truncated: true };
|
|
24
26
|
}
|
|
25
27
|
/**
|
|
26
28
|
* SDK/工具的图片块 → 前端可直接 <img> 的 UiImageBlock。
|
|
@@ -204,13 +206,24 @@ export function serializeMessage(m, seq) {
|
|
|
204
206
|
}
|
|
205
207
|
const id = uiMessageId(m, seq);
|
|
206
208
|
switch (m.role) {
|
|
207
|
-
case "user":
|
|
209
|
+
case "user": {
|
|
210
|
+
const quotes = [];
|
|
211
|
+
const raw = typeof m.content === "string" ? [{ type: "text", text: m.content }] : m.content;
|
|
212
|
+
const content = raw.map((block) => {
|
|
213
|
+
if (block.type !== "text")
|
|
214
|
+
return block;
|
|
215
|
+
const parsed = splitQuotedPrompt(block.text);
|
|
216
|
+
quotes.push(...parsed.quotes);
|
|
217
|
+
return { ...block, text: parsed.text };
|
|
218
|
+
});
|
|
208
219
|
return {
|
|
209
220
|
id,
|
|
210
221
|
role: "user",
|
|
211
|
-
content: serializeUserContent(
|
|
222
|
+
content: serializeUserContent(content),
|
|
223
|
+
...(quotes.length ? { details: { quotes } } : {}),
|
|
212
224
|
timestamp: m.timestamp,
|
|
213
225
|
};
|
|
226
|
+
}
|
|
214
227
|
case "assistant":
|
|
215
228
|
return {
|
|
216
229
|
id,
|
|
@@ -38,6 +38,13 @@ export const NATIVE_COMMANDS = [
|
|
|
38
38
|
argumentHint: "<路径>",
|
|
39
39
|
argumentHintEn: "<path>",
|
|
40
40
|
},
|
|
41
|
+
{
|
|
42
|
+
name: "plan",
|
|
43
|
+
description: "开启或关闭计划模式(/plan [on|off])",
|
|
44
|
+
descriptionEn: "Toggle plan mode (/plan [on|off])",
|
|
45
|
+
argumentHint: "[on|off]",
|
|
46
|
+
argumentHintEn: "[on|off]",
|
|
47
|
+
},
|
|
41
48
|
{
|
|
42
49
|
name: "thinking",
|
|
43
50
|
description: "设置思考强度",
|
|
@@ -249,6 +256,15 @@ export class SlashCommandsService {
|
|
|
249
256
|
await this.host.setCwd(args);
|
|
250
257
|
}
|
|
251
258
|
return true;
|
|
259
|
+
case "plan": {
|
|
260
|
+
const trimmed = args.trim().toLowerCase();
|
|
261
|
+
const current = this.host.getPlanMode ? this.host.getPlanMode() : false;
|
|
262
|
+
const next = trimmed === "on" ? true : trimmed === "off" ? false : !current;
|
|
263
|
+
if (this.host.setPlanMode) {
|
|
264
|
+
await this.host.setPlanMode(next);
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
252
268
|
case "thinking": {
|
|
253
269
|
const ALIAS = {
|
|
254
270
|
off: "off",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* systemPromptEn 为英文(en 用,缺失回落中文);调用方用 pickTemplateDescription/
|
|
20
20
|
* pickTemplatePrompt 按语言选用。
|
|
21
21
|
*/
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
23
|
+
import { writeJsonAtomicSync } from "./atomic-file.js";
|
|
24
24
|
/**
|
|
25
25
|
* 思考强度取值:与 SDK 的 THINKING_LEVEL_OPTIONS(前端 ModelThinking 的 THINKING_VALUES)
|
|
26
26
|
* 一致。SDK 未从包根导出该常量,这里照抄一份作输入校验(写错的值一律当未配置)。
|
|
@@ -608,10 +608,7 @@ export class SubagentTemplatesStore {
|
|
|
608
608
|
}
|
|
609
609
|
persist() {
|
|
610
610
|
try {
|
|
611
|
-
|
|
612
|
-
const tmp = `${this.filePath}.${process.pid}.tmp`;
|
|
613
|
-
writeFileSync(tmp, JSON.stringify(this.templates, null, 2) + "\n");
|
|
614
|
-
renameSync(tmp, this.filePath);
|
|
611
|
+
writeJsonAtomicSync(this.filePath, this.templates);
|
|
615
612
|
}
|
|
616
613
|
catch {
|
|
617
614
|
// best effort
|
|
@@ -644,10 +641,7 @@ export class SubagentTemplatesStore {
|
|
|
644
641
|
saveSeeded(names) {
|
|
645
642
|
this.seededNames = names;
|
|
646
643
|
try {
|
|
647
|
-
|
|
648
|
-
const tmp = `${this.seededPath()}.tmp`;
|
|
649
|
-
writeFileSync(tmp, JSON.stringify([...names].sort(), null, 2));
|
|
650
|
-
renameSync(tmp, this.seededPath());
|
|
644
|
+
writeJsonAtomicSync(this.seededPath(), [...names].sort());
|
|
651
645
|
}
|
|
652
646
|
catch {
|
|
653
647
|
// best effort
|
package/dist/server/subagents.js
CHANGED
|
@@ -31,14 +31,22 @@ import { pick } from "./i18n.js";
|
|
|
31
31
|
/**
|
|
32
32
|
* subagent_wait_all 的最长阻塞时间:必须短暂低于工具看门狗(默认 20 分钟,
|
|
33
33
|
* PI_WEB_TOOL_TIMEOUT_MS 可调),否则看门狗会先中止整个会话而不是让 wait
|
|
34
|
-
* 干净地超时返回。取 0.8
|
|
35
|
-
*
|
|
34
|
+
* 干净地超时返回。取 0.8×看门狗(默认 ~16min,支持 PI_WEB_SUBAGENT_WAIT_CAP_SECONDS 覆盖),
|
|
35
|
+
* 兜底不低于 5s。
|
|
36
36
|
*/
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
37
|
+
export function computeWaitCapMs(watchdogMs) {
|
|
38
|
+
const customSecs = Number(process.env.PI_WEB_SUBAGENT_WAIT_CAP_SECONDS);
|
|
39
|
+
if (Number.isFinite(customSecs) && customSecs > 0) {
|
|
40
|
+
return Math.max(Math.floor(customSecs * 1000), 5_000);
|
|
41
|
+
}
|
|
42
|
+
const envWatchdog = Number(process.env.PI_WEB_TOOL_TIMEOUT_MS);
|
|
43
|
+
const watchdog = typeof watchdogMs === "number" && Number.isFinite(watchdogMs) && watchdogMs > 0
|
|
44
|
+
? watchdogMs
|
|
45
|
+
: Number.isFinite(envWatchdog) && envWatchdog > 0
|
|
46
|
+
? envWatchdog
|
|
47
|
+
: 20 * 60_000;
|
|
48
|
+
return Math.max(Math.floor(watchdog * 0.8), 5_000);
|
|
49
|
+
}
|
|
42
50
|
/** 子代理是否已到终态(运行结束、被中止或出错)。wait 工具据此判断
|
|
43
51
|
* 是否可以取结果;streaming=false 即可(error/canceled 都在快照里带标记)。 */
|
|
44
52
|
export function isSubagentTerminal(r) {
|
|
@@ -93,8 +101,8 @@ export function makeSubagentTool(host, lang, selfConvId) {
|
|
|
93
101
|
details,
|
|
94
102
|
});
|
|
95
103
|
const currentWaitCapMs = typeof host.getWatchdogTimeoutMs === "function"
|
|
96
|
-
?
|
|
97
|
-
:
|
|
104
|
+
? computeWaitCapMs(host.getWatchdogTimeoutMs())
|
|
105
|
+
: computeWaitCapMs();
|
|
98
106
|
const capSeconds = Math.floor(currentWaitCapMs / 1000);
|
|
99
107
|
return defineTool({
|
|
100
108
|
name: SUBAGENT_TOOL_NAME,
|
|
@@ -361,7 +369,10 @@ export function makeSubagentTool(host, lang, selfConvId) {
|
|
|
361
369
|
}
|
|
362
370
|
const allDone = Array.from(wanted).every((id) => {
|
|
363
371
|
const r = host.getSubagent(id);
|
|
364
|
-
|
|
372
|
+
// #488:getSubagent 返回 undefined = 已被移出(强行关闭/收尾清理)
|
|
373
|
+
// ——视为终态,与下方报告段「已移出或不存在」口径一致;否则等待
|
|
374
|
+
// 集合含被移出子代理时 allDone 恒 false,必然白等满 timeoutMs。
|
|
375
|
+
return r === undefined || isSubagentTerminal(r);
|
|
365
376
|
});
|
|
366
377
|
if (allDone)
|
|
367
378
|
break;
|
|
@@ -534,5 +545,5 @@ function promptRemaining(remaining, lang = "en") {
|
|
|
534
545
|
if (remaining.length === 0)
|
|
535
546
|
return "";
|
|
536
547
|
const pendingIds = remaining.map(shortId).join(", ");
|
|
537
|
-
return pick(lang, `\n未完成:${pendingIds}。可再次调用
|
|
548
|
+
return pick(lang, `\n未完成:${pendingIds}。可再次调用 subagent(action="wait_all")继续等待耗时任务,或使用 steer 补充指令 / stop 中止。`, `\nPending: ${pendingIds}. You may call subagent (action="wait_all") again to continue waiting for long tasks, or use steer to add instructions / stop to abort.`, "subagents.wait.pending", { pendingIds: pendingIds });
|
|
538
549
|
}
|
package/dist/server/terminals.js
CHANGED
|
@@ -1889,7 +1889,9 @@ lang, options) {
|
|
|
1889
1889
|
}),
|
|
1890
1890
|
execute: async (_id, p) => {
|
|
1891
1891
|
const lang = getLang();
|
|
1892
|
-
const
|
|
1892
|
+
const safeCols = Math.max(2, Math.min(500, Math.floor(p.cols ?? 120)));
|
|
1893
|
+
const safeRows = Math.max(2, Math.min(200, Math.floor(p.rows ?? 40)));
|
|
1894
|
+
const info = terminals.create(p.terminalId, p.cwd ?? cwd, safeCols, safeRows, cwd, p.terminalId);
|
|
1893
1895
|
const infoJson = JSON.stringify(info);
|
|
1894
1896
|
if (!info)
|
|
1895
1897
|
throw new Error(pick(lang, `创建终端失败:${p.terminalId}`, `Failed to create terminal: ${p.terminalId}`, "terminals.create.failed", { "p.terminalId": p.terminalId }));
|
|
@@ -1977,10 +1979,12 @@ lang, options) {
|
|
|
1977
1979
|
}),
|
|
1978
1980
|
execute: async (_id, p, signal) => {
|
|
1979
1981
|
const lang = getLang();
|
|
1980
|
-
const cursor = p.cursor ?? 0;
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1982
|
+
const cursor = Math.max(0, Math.floor(p.cursor ?? 0));
|
|
1983
|
+
const waitMs = p.waitMs !== undefined ? Math.max(0, Math.min(120_000, Math.floor(p.waitMs))) : undefined;
|
|
1984
|
+
const maxBytes = Math.max(1, Math.min(100_000, Math.floor(p.maxBytes ?? 20000)));
|
|
1985
|
+
if (waitMs)
|
|
1986
|
+
await terminals.waitForOutput(p.terminalId, cursor, waitMs, signal);
|
|
1987
|
+
const read = terminals.read(p.terminalId, cursor, maxBytes);
|
|
1984
1988
|
if (!read)
|
|
1985
1989
|
throw new Error(pick(lang, `终端不存在:${p.terminalId}`, `Terminal not found: ${p.terminalId}`, "terminals.read.not.found", { "p.terminalId": p.terminalId }));
|
|
1986
1990
|
return result(JSON.stringify(read), read);
|
|
@@ -2019,7 +2023,8 @@ lang, options) {
|
|
|
2019
2023
|
return result(JSON.stringify({ applicable: false, reason: why }), { applicable: false });
|
|
2020
2024
|
}
|
|
2021
2025
|
const cursor = p.cursor ?? terminals.endCursor(p.terminalId) ?? 0;
|
|
2022
|
-
const
|
|
2026
|
+
const maxWait = Math.max(100, Math.min(600_000, Math.floor(p.maxWaitMs ?? 300_000)));
|
|
2027
|
+
const wait = await terminals.waitForCompletion(p.terminalId, maxWait, signal, cursor);
|
|
2023
2028
|
const read = terminals.read(p.terminalId, cursor, 20_000);
|
|
2024
2029
|
const outputTail = read?.data ? stripAnsi(read.data).slice(-4000) : "";
|
|
2025
2030
|
return result(JSON.stringify({ ...wait, outputTail }), {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** 校验引用元数据,保留原文的换行和缩进。 */
|
|
2
|
+
export function readTextQuote(value) {
|
|
3
|
+
if (!value || typeof value !== "object")
|
|
4
|
+
return null;
|
|
5
|
+
const q = value;
|
|
6
|
+
if (typeof q.text !== "string" ||
|
|
7
|
+
!q.text.trim() ||
|
|
8
|
+
typeof q.messageId !== "string" ||
|
|
9
|
+
!q.messageId ||
|
|
10
|
+
typeof q.role !== "string" ||
|
|
11
|
+
!q.role)
|
|
12
|
+
return null;
|
|
13
|
+
return {
|
|
14
|
+
text: q.text,
|
|
15
|
+
messageId: q.messageId,
|
|
16
|
+
role: q.role,
|
|
17
|
+
...(typeof q.sessionId === "string" && q.sessionId ? { sessionId: q.sessionId } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** JSON 字符串保留原文边界,引用中的换行不参与外层标记解析。 */
|
|
21
|
+
export function formatTextQuote(quote) {
|
|
22
|
+
return `<quoted-text>\n${JSON.stringify(quote)}\n</quoted-text>`;
|
|
23
|
+
}
|
|
24
|
+
export function parseTextQuote(text) {
|
|
25
|
+
const match = /^<quoted-text>\n([^\n]*)\n<\/quoted-text>$/.exec(text);
|
|
26
|
+
if (!match)
|
|
27
|
+
return null;
|
|
28
|
+
try {
|
|
29
|
+
return readTextQuote(JSON.parse(match[1]));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function formatQuotedPrompt(text, quotes) {
|
|
36
|
+
return [text, ...quotes.map(formatTextQuote)].join("\n\n");
|
|
37
|
+
}
|
|
38
|
+
/** 排队项把引用与提问一起保存;展示时只拆分末尾的完整引用块。 */
|
|
39
|
+
export function splitQuotedPrompt(value) {
|
|
40
|
+
let text = value;
|
|
41
|
+
const quotes = [];
|
|
42
|
+
while (true) {
|
|
43
|
+
const match = /(?:^|\n\n)(<quoted-text>\n[^\n]*\n<\/quoted-text>)$/.exec(text);
|
|
44
|
+
const quote = match ? parseTextQuote(match[1]) : null;
|
|
45
|
+
if (!match || !quote)
|
|
46
|
+
break;
|
|
47
|
+
quotes.unshift(quote);
|
|
48
|
+
text = text.slice(0, match.index);
|
|
49
|
+
}
|
|
50
|
+
return { text, quotes };
|
|
51
|
+
}
|
|
52
|
+
export function messageTextWithQuotes(message) {
|
|
53
|
+
const text = message.content
|
|
54
|
+
.map((block) => (block.type === "text" && typeof block.text === "string" ? block.text : ""))
|
|
55
|
+
.join("\n");
|
|
56
|
+
const values = message.details?.quotes;
|
|
57
|
+
const quotes = Array.isArray(values) ? values.map(readTextQuote).filter((q) => q !== null) : [];
|
|
58
|
+
return formatQuotedPrompt(text, quotes);
|
|
59
|
+
}
|
|
@@ -345,10 +345,19 @@ export function applyAgentToolsGating(session, disabled, preset) {
|
|
|
345
345
|
const allNames = session.getAllTools().map((t) => t.name);
|
|
346
346
|
const names = new Set(allNames);
|
|
347
347
|
for (const t of AGENT_TOOL_CATALOG) {
|
|
348
|
-
if (off.has(t.name))
|
|
349
|
-
|
|
350
|
-
|
|
348
|
+
if (off.has(t.name)) {
|
|
349
|
+
// issue #481: 若第三方扩展注册了同名工具(如 nicobailon/pi-subagents 的 subagent),
|
|
350
|
+
// 关闭内置工具时允许扩展工具透传,不从活跃名单删除
|
|
351
|
+
const hasExtensionTool = Boolean(session?.extensionRunner
|
|
352
|
+
?.getAllRegisteredTools?.()
|
|
353
|
+
?.some((tool) => tool.definition?.name === t.name));
|
|
354
|
+
if (!hasExtensionTool) {
|
|
355
|
+
names.delete(t.name);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
351
359
|
names.add(t.name);
|
|
360
|
+
}
|
|
352
361
|
}
|
|
353
362
|
for (const core of CORE_BUILTIN_TOOL_NAMES) {
|
|
354
363
|
if (off.has(core))
|
|
@@ -57,3 +57,27 @@ export function installToolOverrides(session, specs) {
|
|
|
57
57
|
session._refreshToolRegistry();
|
|
58
58
|
return [...names];
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* 同步 subagent 工具与第三方扩展的同名工具共存(issue #481):
|
|
62
|
+
* 当第三方扩展注册了同名 subagent 时,若用户在设置中关闭/禁用了内置 subagent,
|
|
63
|
+
* 则自动从 session._customTools 中移除内置 subagent,让扩展的同名工具透传生效;
|
|
64
|
+
* 重新启用时则重新注入内置 subagent。
|
|
65
|
+
*/
|
|
66
|
+
export function syncSubagentOverride(session, subagentDisabled, builtinSubagentTool) {
|
|
67
|
+
if (!Array.isArray(session._customTools) || typeof session._refreshToolRegistry !== "function")
|
|
68
|
+
return;
|
|
69
|
+
const hasExtensionTool = Boolean(extensionToolDefinition(session, "subagent"));
|
|
70
|
+
if (!hasExtensionTool)
|
|
71
|
+
return;
|
|
72
|
+
const idx = session._customTools.findIndex((t) => t.name === "subagent");
|
|
73
|
+
if (subagentDisabled) {
|
|
74
|
+
if (idx !== -1) {
|
|
75
|
+
session._customTools = session._customTools.filter((t) => t.name !== "subagent");
|
|
76
|
+
session._refreshToolRegistry();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (idx === -1 && builtinSubagentTool) {
|
|
80
|
+
session._customTools = [builtinSubagentTool(), ...session._customTools];
|
|
81
|
+
session._refreshToolRegistry();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -375,29 +375,43 @@ function piCliOnPath() {
|
|
|
375
375
|
}
|
|
376
376
|
return null;
|
|
377
377
|
}
|
|
378
|
+
/** Read a pi-core package.json's version, or null when absent, not ours, or
|
|
379
|
+
* unreadable. Shared by both resolution strategies below. */
|
|
380
|
+
function readPiCoreVersionAt(pkgPath) {
|
|
381
|
+
if (!existsSync(pkgPath))
|
|
382
|
+
return null;
|
|
383
|
+
try {
|
|
384
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
385
|
+
if (pkg.name === PI_CORE_PACKAGE && pkg.version)
|
|
386
|
+
return pkg.version;
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
/* unreadable package.json */
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
378
393
|
/**
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
394
|
+
* <binDir>/node_modules/<pkg>/package.json — the concrete layout a global npm
|
|
395
|
+
* install always uses. This is the FIRST candidate because it is the only one
|
|
396
|
+
* that works on Windows: npm's cmd-shim writes a regular `pi` shell script
|
|
397
|
+
* (not a symlink into the package), so the realpath walk below would start
|
|
398
|
+
* from <prefix> itself and never see the package one level down (issue #533).
|
|
382
399
|
*/
|
|
383
|
-
function
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
400
|
+
function versionFromSiblingNodeModules(bin) {
|
|
401
|
+
return readPiCoreVersionAt(join(dirname(bin), "node_modules", ...PI_CORE_PACKAGE.split("/"), "package.json"));
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* POSIX: `pi` is a symlink into <prefix>/node_modules/<pkg>/dist/bundle/cli.js
|
|
405
|
+
* — realpath it and walk up (≤8 levels) until a package.json names our
|
|
406
|
+
* package. Functionally unchanged from the original probe.
|
|
407
|
+
*/
|
|
408
|
+
function versionFromSymlinkWalk(bin) {
|
|
387
409
|
try {
|
|
388
410
|
let dir = dirname(realpathSync(bin));
|
|
389
411
|
for (let i = 0; i < 8; i++) {
|
|
390
|
-
const
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
394
|
-
if (pkg.name === PI_CORE_PACKAGE && pkg.version)
|
|
395
|
-
return pkg.version;
|
|
396
|
-
}
|
|
397
|
-
catch {
|
|
398
|
-
/* unreadable package.json — keep walking */
|
|
399
|
-
}
|
|
400
|
-
}
|
|
412
|
+
const version = readPiCoreVersionAt(join(dir, "package.json"));
|
|
413
|
+
if (version)
|
|
414
|
+
return version;
|
|
401
415
|
const parent = dirname(dir);
|
|
402
416
|
if (parent === dir)
|
|
403
417
|
break;
|
|
@@ -409,6 +423,17 @@ function readPiCoreVersionFromDisk() {
|
|
|
409
423
|
}
|
|
410
424
|
return null;
|
|
411
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Read the pi core version from disk: resolve the `pi` bin on PATH, then try
|
|
428
|
+
* the concrete global-npm layout first (Windows) and the POSIX symlink walk
|
|
429
|
+
* second. Exported for unit tests — defaultProbePiCore memoizes its result.
|
|
430
|
+
*/
|
|
431
|
+
export function readPiCoreVersionFromDisk() {
|
|
432
|
+
const bin = piCliOnPath();
|
|
433
|
+
if (!bin)
|
|
434
|
+
return null;
|
|
435
|
+
return versionFromSiblingNodeModules(bin) ?? versionFromSymlinkWalk(bin);
|
|
436
|
+
}
|
|
412
437
|
/**
|
|
413
438
|
* Default pi core probe: run the globally installed `pi --version`, memoized
|
|
414
439
|
* machine-wide for PI_PROBE_TTL_MS so repeated collectTargets calls never
|
|
@@ -485,13 +510,16 @@ export function isHostProvidedPackage(name) {
|
|
|
485
510
|
export function collectTargets(agentDir, webuiVersion, probePiCore = defaultProbePiCore, opts) {
|
|
486
511
|
const targets = [{ name: "pi-web-ui", version: webuiVersion, kind: "webui" }];
|
|
487
512
|
const coreVersion = probePiCore() ?? readVendoredPiCore(agentDir);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
513
|
+
// issue #533: keep the pi-core row even when the version is undetectable
|
|
514
|
+
// (e.g. unreadable global install) instead of silently dropping it — an
|
|
515
|
+
// honest "could not detect" row beats a missing one. checkAll resolves the
|
|
516
|
+
// key into the item's localized `error`.
|
|
517
|
+
targets.push({
|
|
518
|
+
name: PI_CORE_PACKAGE,
|
|
519
|
+
version: coreVersion ?? "unknown",
|
|
520
|
+
kind: "pi-core",
|
|
521
|
+
...(coreVersion ? {} : { error: "pluginupdate.piCore.unknown" }),
|
|
522
|
+
});
|
|
495
523
|
targets.push(...listInstalledPackages(agentDir).filter((pkg) => !isHostProvidedPackage(pkg.name)));
|
|
496
524
|
targets.push(...listGitExtensions(agentDir, opts?.projectCwd));
|
|
497
525
|
return targets;
|
|
@@ -636,6 +664,22 @@ gitCheck = defaultCheckGitExtension) {
|
|
|
636
664
|
while (cursor < targets.length) {
|
|
637
665
|
const i = cursor++;
|
|
638
666
|
const t = targets[i];
|
|
667
|
+
if (t.error) {
|
|
668
|
+
// Pre-flagged target (issue #533, e.g. undetectable pi-core
|
|
669
|
+
// version): `error` carries an i18n key resolved here so
|
|
670
|
+
// collectTargets stays language-free. No registry lookup.
|
|
671
|
+
results[i] = {
|
|
672
|
+
name: t.name,
|
|
673
|
+
kind: t.kind,
|
|
674
|
+
current: t.version,
|
|
675
|
+
latest: null,
|
|
676
|
+
latestPublishedAt: null,
|
|
677
|
+
upToDate: false,
|
|
678
|
+
...(t.source ? { source: t.source } : {}),
|
|
679
|
+
error: pick(l, "无法检测已安装的引擎版本(可能未装全局 pi,或安装方式不标准),可手动检查。", "Could not detect the installed engine version (no global pi, or a non-standard install); check manually.", t.error),
|
|
680
|
+
};
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
639
683
|
if (t.kind === "git-extension") {
|
|
640
684
|
try {
|
|
641
685
|
if (!t.installDir)
|
package/extensions/webui.ts
CHANGED
|
@@ -215,14 +215,28 @@ export default function (pi: ExtensionAPI): void {
|
|
|
215
215
|
const cwd = opts.cwd ?? ctx.cwd;
|
|
216
216
|
const url = `http://localhost:${port}`;
|
|
217
217
|
|
|
218
|
+
// 探测并向子进程显式透传宿主 Pi SDK 路径(issue #482)
|
|
219
|
+
let hostSdkDir: string | undefined;
|
|
220
|
+
try {
|
|
221
|
+
const resolved = import.meta.resolve?.("@earendil-works/pi-coding-agent");
|
|
222
|
+
if (resolved) {
|
|
223
|
+
let p = resolved.startsWith("file:") ? fileURLToPath(resolved) : resolved;
|
|
224
|
+
if (p.endsWith("index.js") || p.endsWith("index.mjs")) p = dirname(p);
|
|
225
|
+
if (existsSync(join(p, "package.json"))) hostSdkDir = p;
|
|
226
|
+
}
|
|
227
|
+
} catch {}
|
|
228
|
+
|
|
218
229
|
const env = {
|
|
219
230
|
...process.env,
|
|
220
231
|
PORT: String(port),
|
|
221
232
|
PI_WEB_PORT: String(port), // server/index.js 读取 PI_WEB_PORT
|
|
222
233
|
PI_WEB_CWD: cwd,
|
|
234
|
+
...(hostSdkDir ? { PI_WEB_SDK_DIR: hostSdkDir } : {}),
|
|
223
235
|
...(process.env.PI_WEB_DATA_DIR ? {} : { PI_WEB_DATA_DIR: join(cwd, ".pi-web") }),
|
|
224
236
|
};
|
|
225
|
-
const
|
|
237
|
+
const hookPath = join(PKG_ROOT, "dist", "server", "resolve-global-sdk.js");
|
|
238
|
+
const nodeArgs = existsSync(hookPath) ? ["--import", hookPath, SERVER_ENTRY] : [SERVER_ENTRY];
|
|
239
|
+
const proc = spawn(NODE, nodeArgs, {
|
|
226
240
|
cwd,
|
|
227
241
|
env,
|
|
228
242
|
stdio: "ignore",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.98.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": {
|
|
@@ -79,39 +79,35 @@
|
|
|
79
79
|
"desktop:dist": "npm run build && npm run build:desktop && electron-builder -c desktop/electron-builder.yml",
|
|
80
80
|
"format": "prettier --write server/ web/src/ extensions/ scripts/ bin/ desktop/ tests/*.mjs tests/unit/ *.mjs",
|
|
81
81
|
"format:check": "prettier --check server/ web/src/ extensions/ scripts/ bin/ desktop/ tests/*.mjs tests/unit/ *.mjs",
|
|
82
|
-
"lint": "oxlint server/ web/src/ extensions/ scripts/ desktop/",
|
|
83
|
-
"lint:fix": "oxlint --fix server/ web/src/ extensions/ scripts/ desktop/"
|
|
82
|
+
"lint": "oxlint server/ web/src/ extensions/ scripts/ bin/ desktop/",
|
|
83
|
+
"lint:fix": "oxlint --fix server/ web/src/ extensions/ scripts/ bin/ desktop/"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@deepseek-ai/dsh-sdk-jsonrpc-server": "^0.1.5-rc.3",
|
|
87
87
|
"@deepseek-ai/dsh-sdk-protocol": "^0.1.5-rc.3",
|
|
88
88
|
"@earendil-works/pi-coding-agent": ">=0.85.1",
|
|
89
|
-
"@xterm/addon-fit": "^0.11.0",
|
|
90
|
-
"@xterm/xterm": "^6.0.0",
|
|
91
89
|
"compression": "^1.8.2",
|
|
92
90
|
"electron-updater": "^6.8.9",
|
|
93
91
|
"express": "^5.2.1",
|
|
94
|
-
"highlight.js": "^11.12.0",
|
|
95
|
-
"html-to-image": "^1.11.13",
|
|
96
|
-
"katex": "^0.18.7",
|
|
97
92
|
"node-pty": "^1.1.0",
|
|
98
|
-
"react": "^19.3.0",
|
|
99
|
-
"react-dom": "^19.3.0",
|
|
100
|
-
"react-icons": "^5.7.0",
|
|
101
|
-
"react-markdown": "^10.1.0",
|
|
102
|
-
"rehype-highlight": "^7.0.2",
|
|
103
|
-
"rehype-katex": "^7.0.1",
|
|
104
|
-
"rehype-raw": "^7.0.0",
|
|
105
|
-
"rehype-sanitize": "^6.0.0",
|
|
106
|
-
"remark-breaks": "^4.0.0",
|
|
107
|
-
"remark-gfm": "^4.0.1",
|
|
108
|
-
"remark-math": "^6.0.0",
|
|
109
93
|
"tar": "^7.5.22",
|
|
110
94
|
"typebox": "^1.3.34",
|
|
111
95
|
"undici": "^8.9.0",
|
|
112
96
|
"ws": "^8.21.3",
|
|
113
97
|
"yauzl": "^3.4.0"
|
|
114
98
|
},
|
|
99
|
+
"peerDependencies": {
|
|
100
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
101
|
+
"typebox": "*"
|
|
102
|
+
},
|
|
103
|
+
"peerDependenciesMeta": {
|
|
104
|
+
"@earendil-works/pi-coding-agent": {
|
|
105
|
+
"optional": true
|
|
106
|
+
},
|
|
107
|
+
"typebox": {
|
|
108
|
+
"optional": true
|
|
109
|
+
}
|
|
110
|
+
},
|
|
115
111
|
"devDependencies": {
|
|
116
112
|
"@types/compression": "^1.8.1",
|
|
117
113
|
"@types/express": "^5.0.6",
|
|
@@ -121,16 +117,32 @@
|
|
|
121
117
|
"@types/ws": "^8.18.1",
|
|
122
118
|
"@types/yauzl": "^3.4.0",
|
|
123
119
|
"@vitejs/plugin-react": "^6.1.1",
|
|
120
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
121
|
+
"@xterm/xterm": "^6.0.0",
|
|
124
122
|
"concurrently": "^10.0.5",
|
|
125
123
|
"cross-env": "^10.1.0",
|
|
126
124
|
"electron": "^44.4.2",
|
|
127
125
|
"electron-builder": "^26.15.3",
|
|
128
126
|
"esbuild": "^0.28.2",
|
|
127
|
+
"highlight.js": "^11.12.0",
|
|
128
|
+
"html-to-image": "^1.11.13",
|
|
129
129
|
"jsdom": "^30.1.0",
|
|
130
|
+
"katex": "^0.18.7",
|
|
130
131
|
"mermaid": "^12.0.0",
|
|
131
132
|
"oxlint": "^1.83.0",
|
|
132
133
|
"playwright-core": "^1.63.0",
|
|
133
134
|
"prettier": "^3.9.8",
|
|
135
|
+
"react": "^19.3.0",
|
|
136
|
+
"react-dom": "^19.3.0",
|
|
137
|
+
"react-icons": "^5.7.0",
|
|
138
|
+
"react-markdown": "^10.1.0",
|
|
139
|
+
"rehype-highlight": "^7.0.2",
|
|
140
|
+
"rehype-katex": "^7.0.1",
|
|
141
|
+
"rehype-raw": "^7.0.0",
|
|
142
|
+
"rehype-sanitize": "^6.0.0",
|
|
143
|
+
"remark-breaks": "^4.0.0",
|
|
144
|
+
"remark-gfm": "^4.0.1",
|
|
145
|
+
"remark-math": "^6.0.0",
|
|
134
146
|
"tsx": "^4.23.13",
|
|
135
147
|
"typescript": "^7.0.2",
|
|
136
148
|
"vis-timeline": "^8.5.4",
|