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
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
|
+
}
|
|
@@ -187,21 +187,27 @@ export function looksLikeText(buf) {
|
|
|
187
187
|
}
|
|
188
188
|
return control / Math.max(text.length, 1) < 0.02;
|
|
189
189
|
}
|
|
190
|
-
/** Decode bytes: strict UTF-8 first
|
|
191
|
-
*
|
|
192
|
-
*
|
|
190
|
+
/** Decode bytes: strict UTF-8 first (retrying with up to 3 truncated tail
|
|
191
|
+
* bytes removed for excerpt/preview buffers cut mid-character), falling back
|
|
192
|
+
* to GBK (Windows legacy Chinese files), then latin1 as a last resort — so
|
|
193
|
+
* previews and inline attachments never show mojibake for GBK/GB2312 files
|
|
194
|
+
* nor for truncated UTF-8 files (issue #392). */
|
|
193
195
|
export function decodeText(buf) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
196
|
+
for (let cut = 0; cut < 4; cut++) {
|
|
197
|
+
const slice = cut === 0 ? buf : buf.subarray(0, Math.max(0, buf.length - cut));
|
|
198
198
|
try {
|
|
199
|
-
return new TextDecoder("
|
|
199
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(slice);
|
|
200
200
|
}
|
|
201
201
|
catch {
|
|
202
|
-
|
|
202
|
+
// Tail byte might be cut mid-character; try trimming up to 3 bytes.
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
|
+
try {
|
|
206
|
+
return new TextDecoder("gbk").decode(buf);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return buf.toString("latin1");
|
|
210
|
+
}
|
|
205
211
|
}
|
|
206
212
|
/** Sniff an image MIME type from magic bytes (extension is only a hint).
|
|
207
213
|
* Returns null when the bytes don't look like a known raster format —
|
|
@@ -25,8 +25,11 @@ export const TERMINAL_TOOL_NAMES = [
|
|
|
25
25
|
"terminal_read",
|
|
26
26
|
"terminal_wait",
|
|
27
27
|
];
|
|
28
|
-
/** 第一方子代理工具(定义见 subagents.ts
|
|
29
|
-
export const
|
|
28
|
+
/** 第一方子代理工具(定义见 subagents.ts,统一为单 action 工具)。 */
|
|
29
|
+
export const SUBAGENT_TOOL_NAME = "subagent";
|
|
30
|
+
export const SUBAGENT_TOOL_NAMES = [SUBAGENT_TOOL_NAME];
|
|
31
|
+
/** 旧版 8 个独立子代理工具名(持久化配置迁移用)。 */
|
|
32
|
+
export const LEGACY_SUBAGENT_TOOL_NAMES = [
|
|
30
33
|
"subagent_spawn",
|
|
31
34
|
"subagent_get_result",
|
|
32
35
|
"subagent_steer",
|
|
@@ -251,7 +254,7 @@ export const AGENT_TOOL_CATALOG = [
|
|
|
251
254
|
},
|
|
252
255
|
];
|
|
253
256
|
/** SDK 核心内置工具名(可被门控显式禁用或被预设白名单过滤)。 */
|
|
254
|
-
export const CORE_BUILTIN_TOOL_NAMES = ["bash", "read", "edit", "write"];
|
|
257
|
+
export const CORE_BUILTIN_TOOL_NAMES = ["bash", "read", "edit", "write", "powershell", "ls", "grep", "find"];
|
|
255
258
|
export function isCoreBuiltinTool(name) {
|
|
256
259
|
return CORE_BUILTIN_TOOL_NAMES.includes(name);
|
|
257
260
|
}
|
|
@@ -268,7 +271,11 @@ export function normalizeDisabledAgentTools(v) {
|
|
|
268
271
|
const out = [];
|
|
269
272
|
for (const x of v) {
|
|
270
273
|
// 旧名迁移:markers_list → todo_list(改名前已关闭的用户保持关闭)。
|
|
271
|
-
|
|
274
|
+
let name = x === LEGACY_MARKERS_LIST_TOOL_NAME ? MARKERS_LIST_TOOL_NAME : x;
|
|
275
|
+
// 旧版子代理工具迁移:任意旧 subagent_* 关闭均迁移为关闭 subagent 工具。
|
|
276
|
+
if (typeof name === "string" && LEGACY_SUBAGENT_TOOL_NAMES.includes(name)) {
|
|
277
|
+
name = SUBAGENT_TOOL_NAME;
|
|
278
|
+
}
|
|
272
279
|
if (typeof name === "string" && (KNOWN_NAMES.has(name) || isCoreBuiltinTool(name)) && !out.includes(name)) {
|
|
273
280
|
out.push(name);
|
|
274
281
|
}
|
|
@@ -338,10 +345,19 @@ export function applyAgentToolsGating(session, disabled, preset) {
|
|
|
338
345
|
const allNames = session.getAllTools().map((t) => t.name);
|
|
339
346
|
const names = new Set(allNames);
|
|
340
347
|
for (const t of AGENT_TOOL_CATALOG) {
|
|
341
|
-
if (off.has(t.name))
|
|
342
|
-
|
|
343
|
-
|
|
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 {
|
|
344
359
|
names.add(t.name);
|
|
360
|
+
}
|
|
345
361
|
}
|
|
346
362
|
for (const core of CORE_BUILTIN_TOOL_NAMES) {
|
|
347
363
|
if (off.has(core))
|
|
@@ -363,7 +379,9 @@ export const PI_AGENT_PRESETS = [
|
|
|
363
379
|
trust: "system",
|
|
364
380
|
isDefault: true,
|
|
365
381
|
name: "全功能",
|
|
382
|
+
nameEn: "Full access",
|
|
366
383
|
description: "提供全部可用工具与扩展能力(默认)",
|
|
384
|
+
descriptionEn: "All available tools and extension capabilities (default)",
|
|
367
385
|
order: 0,
|
|
368
386
|
},
|
|
369
387
|
{
|
|
@@ -371,7 +389,9 @@ export const PI_AGENT_PRESETS = [
|
|
|
371
389
|
trust: "system",
|
|
372
390
|
isDefault: false,
|
|
373
391
|
name: "极简模式",
|
|
392
|
+
nameEn: "Minimal",
|
|
374
393
|
description: "仅保留 bash 与 read;插件工具、技能名录与终端引导同步隐藏",
|
|
394
|
+
descriptionEn: "Keeps only bash and read; plugin tools, skill catalog and terminal guidance are hidden too",
|
|
375
395
|
order: 1,
|
|
376
396
|
},
|
|
377
397
|
{
|
|
@@ -379,7 +399,9 @@ export const PI_AGENT_PRESETS = [
|
|
|
379
399
|
trust: "system",
|
|
380
400
|
isDefault: false,
|
|
381
401
|
name: "代码开发",
|
|
402
|
+
nameEn: "Code development",
|
|
382
403
|
description: "专注于代码读写与执行(bash, read, edit, write, edit_soft);插件工具与技能名录同步隐藏",
|
|
404
|
+
descriptionEn: "Focused on reading, writing and running code (bash, read, edit, write, edit_soft); plugin tools and skill catalog are hidden too",
|
|
383
405
|
order: 2,
|
|
384
406
|
},
|
|
385
407
|
{
|
|
@@ -387,7 +409,9 @@ export const PI_AGENT_PRESETS = [
|
|
|
387
409
|
trust: "system",
|
|
388
410
|
isDefault: false,
|
|
389
411
|
name: "只读分析",
|
|
412
|
+
nameEn: "Read-only analysis",
|
|
390
413
|
description: "仅保留只读工具,禁止写操作;插件工具同步隐藏(读写未知,保守处理)",
|
|
414
|
+
descriptionEn: "Keeps only read-only tools and forbids writes; plugin tools are hidden too (read/write unknown, handled conservatively)",
|
|
391
415
|
order: 3,
|
|
392
416
|
},
|
|
393
417
|
{
|
|
@@ -395,7 +419,9 @@ export const PI_AGENT_PRESETS = [
|
|
|
395
419
|
trust: "system",
|
|
396
420
|
isDefault: false,
|
|
397
421
|
name: "纯对话",
|
|
422
|
+
nameEn: "Chat only",
|
|
398
423
|
description: "无工具问答模式,模型不调用任何工具;插件工具与技能名录同步隐藏",
|
|
424
|
+
descriptionEn: "No-tools Q&A: the model never calls a tool; plugin tools and skill catalog are hidden too",
|
|
399
425
|
order: 4,
|
|
400
426
|
},
|
|
401
427
|
];
|
|
@@ -404,19 +430,45 @@ export const PI_PERMISSION_OPTIONS = [
|
|
|
404
430
|
{
|
|
405
431
|
value: "read-only",
|
|
406
432
|
name: "只读模式",
|
|
433
|
+
nameEn: "Read Only",
|
|
407
434
|
description: "禁止所有文件修改(write/edit/edit_soft)及任何非只读操作",
|
|
435
|
+
descriptionEn: "Forbids all file modification (write/edit/edit_soft) and any non-read-only operation",
|
|
408
436
|
},
|
|
409
437
|
{
|
|
410
438
|
value: "workspace-write-never",
|
|
411
439
|
name: "工作区内修改",
|
|
440
|
+
nameEn: "Workspace Write",
|
|
412
441
|
description: "仅允许在当前工作区目录下修改文件,工作区外写操作一律拒绝",
|
|
442
|
+
descriptionEn: "Only files under the current workspace may be modified; writes outside it are always denied",
|
|
413
443
|
},
|
|
414
444
|
{
|
|
415
445
|
value: "danger-full-access",
|
|
416
446
|
name: "完全权限",
|
|
447
|
+
nameEn: "Full access",
|
|
417
448
|
description: "允许修改任意目录文件及执行全量操作(需要二次确认)",
|
|
449
|
+
descriptionEn: "Allows modifying files anywhere and running any operation (needs a second confirmation)",
|
|
418
450
|
},
|
|
419
451
|
];
|
|
452
|
+
/**
|
|
453
|
+
* 预设/权限文案随界面语言落定:中文界面用服务端默认文案,其它语言用
|
|
454
|
+
* nameEn ?? name(与审批规则的 labelEn/reasonEn、插件的 label/labelEn 同一约定;
|
|
455
|
+
* 其它语言包同理回落英文)。服务端 notice 的 textEn 也走这里,否则英文界面会看到
|
|
456
|
+
* `Switched to preset "全功能"`。
|
|
457
|
+
*
|
|
458
|
+
* lang 是 ServerLang(resolveServerLang 的产物):只有 "zh" 算中文。
|
|
459
|
+
* 本文件保持零依赖,故直接比字符串而不 import server/i18n.js。
|
|
460
|
+
*/
|
|
461
|
+
export function localizedName(item, lang, fallback = "") {
|
|
462
|
+
if (!item)
|
|
463
|
+
return fallback;
|
|
464
|
+
return lang === "zh" ? (item.name ?? item.nameEn ?? fallback) : (item.nameEn ?? item.name ?? fallback);
|
|
465
|
+
}
|
|
466
|
+
/** localizedName 的描述版本(description 两边都可缺省 → undefined)。 */
|
|
467
|
+
export function localizedDescription(item, lang) {
|
|
468
|
+
if (!item)
|
|
469
|
+
return undefined;
|
|
470
|
+
return lang === "zh" ? (item.description ?? item.descriptionEn) : (item.descriptionEn ?? item.description);
|
|
471
|
+
}
|
|
420
472
|
/** 按预设过滤活跃工具名。 */
|
|
421
473
|
export function filterToolsByPreset(tools, preset) {
|
|
422
474
|
const all = Array.from(tools);
|
|
@@ -437,6 +489,7 @@ export function filterToolsByPreset(tools, preset) {
|
|
|
437
489
|
"edit",
|
|
438
490
|
"edit_soft",
|
|
439
491
|
"bash",
|
|
492
|
+
"powershell",
|
|
440
493
|
"terminal_create",
|
|
441
494
|
"terminal_input",
|
|
442
495
|
"terminal_close",
|
|
@@ -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
|
|
@@ -466,25 +491,36 @@ function readVendoredPiCore(agentDir) {
|
|
|
466
491
|
return null;
|
|
467
492
|
}
|
|
468
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* 宿主 SDK 及其 peer/内部包(@earendil-works/* / @mariozechner/*):
|
|
496
|
+
* 属于宿主引擎随核心一同分发或由别名提供的 SDK 内部构件,非用户安装的独立扩展组件;
|
|
497
|
+
* 既不应在组件列表重复展示,也不能被 `pi update npm:<name>` 更新(会报 No matching package found,issue #393)。
|
|
498
|
+
*/
|
|
499
|
+
export function isHostProvidedPackage(name) {
|
|
500
|
+
return name === PI_CORE_PACKAGE || name.startsWith("@earendil-works/") || name.startsWith("@mariozechner/");
|
|
501
|
+
}
|
|
469
502
|
/**
|
|
470
503
|
* Build the full local target list: webui + the pi core + installed packages
|
|
471
504
|
* + git-source extensions (issue #178) from global and project settings.
|
|
472
505
|
* The pi core version comes from the CLI probe (injectable for tests), falling
|
|
473
506
|
* back to the vendored copy under <agentDir>/npm/node_modules. Packages
|
|
474
|
-
* listing the core directly are filtered out so the
|
|
475
|
-
*
|
|
507
|
+
* listing the core directly or host-provided SDK peers are filtered out so the
|
|
508
|
+
* pi-core row wins and host peers are not listed as updatable (issue #393).
|
|
476
509
|
*/
|
|
477
510
|
export function collectTargets(agentDir, webuiVersion, probePiCore = defaultProbePiCore, opts) {
|
|
478
511
|
const targets = [{ name: "pi-web-ui", version: webuiVersion, kind: "webui" }];
|
|
479
512
|
const coreVersion = probePiCore() ?? readVendoredPiCore(agentDir);
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
+
});
|
|
523
|
+
targets.push(...listInstalledPackages(agentDir).filter((pkg) => !isHostProvidedPackage(pkg.name)));
|
|
488
524
|
targets.push(...listGitExtensions(agentDir, opts?.projectCwd));
|
|
489
525
|
return targets;
|
|
490
526
|
}
|
|
@@ -628,6 +664,22 @@ gitCheck = defaultCheckGitExtension) {
|
|
|
628
664
|
while (cursor < targets.length) {
|
|
629
665
|
const i = cursor++;
|
|
630
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
|
+
}
|
|
631
683
|
if (t.kind === "git-extension") {
|
|
632
684
|
try {
|
|
633
685
|
if (!t.installDir)
|
|
@@ -239,12 +239,16 @@ export function hasPendingWaitSubscription(options) {
|
|
|
239
239
|
/**
|
|
240
240
|
* 纯函数版置换决策:true = 保留(不得 dispose),false = 调用方可释放。
|
|
241
241
|
* Pure decision core of displaceActive(): true = retain, false = may dispose.
|
|
242
|
-
* 顺序与 displaceActive 保持一致:review/wizard → streaming → terminals →
|
|
242
|
+
* 顺序与 displaceActive 保持一致:pin → review/wizard → streaming → terminals →
|
|
243
243
|
* active subagent run → pending wake → listed+continued(「打开后继续过」的会话也保留)。
|
|
244
244
|
*/
|
|
245
245
|
export function shouldRetainActive(input) {
|
|
246
|
+
if (input.pinned)
|
|
247
|
+
return true;
|
|
246
248
|
if (input.reviewing || input.wizardRunning)
|
|
247
249
|
return true;
|
|
250
|
+
if (input.promptInFlight)
|
|
251
|
+
return true;
|
|
248
252
|
if (input.streaming || input.compacting)
|
|
249
253
|
return true;
|
|
250
254
|
if (input.openTerminals > 0)
|
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
|
-
"@deepseek-ai/dsh-sdk-jsonrpc-server": "^0.1.
|
|
87
|
-
"@deepseek-ai/dsh-sdk-protocol": "^0.1.
|
|
86
|
+
"@deepseek-ai/dsh-sdk-jsonrpc-server": "^0.1.5-rc.3",
|
|
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",
|