pi-web-ui 0.97.0 → 0.99.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 +126 -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 +645 -147
- 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 +30 -5
- package/dist/server/goal-review-gate.js +7 -20
- package/dist/server/goal-service.js +77 -10
- 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 +2 -3
- package/dist/server/patch-tool.js +78 -28
- package/dist/server/permission-preset.js +25 -0
- package/dist/server/plan-manager.js +196 -10
- 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 +24 -12
- 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 -40
- 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-NerrJlTS.js} +1 -1
- package/web/dist/assets/index-BdMaGCcs.css +1 -0
- package/web/dist/assets/index-ByCBN4Kt.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
|
@@ -532,7 +532,9 @@ export class GoalService {
|
|
|
532
532
|
conv.goalGeneration += 1;
|
|
533
533
|
// 上一轮目标可能还留着常驻执行者子代理(委托执行):先停掉再开新的,
|
|
534
534
|
// 否则旧循环会在新目标上继续派活(代次已作废,但角色对话得收)。
|
|
535
|
-
|
|
535
|
+
// await 到 dismiss 完全落定:后面的 spawn 名额检查才不会数到还在收尾的
|
|
536
|
+
// 旧执行者而误报上限(issue #464;stopDelegated 自身已按会话串行化)。
|
|
537
|
+
await this.stopDelegated(conv);
|
|
536
538
|
const goal = conv.goal;
|
|
537
539
|
goal.reviewing = false;
|
|
538
540
|
goal.conversationId = goalConversationId;
|
|
@@ -817,10 +819,15 @@ export class GoalService {
|
|
|
817
819
|
aborted = true;
|
|
818
820
|
};
|
|
819
821
|
ac.signal.addEventListener("abort", onAbort, { once: true });
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
822
|
+
let ans;
|
|
823
|
+
try {
|
|
824
|
+
const choose = isChoice ? ctx.ui.select(qTitle, params.options) : ctx.ui.input(qTitle);
|
|
825
|
+
ans = (await choose);
|
|
826
|
+
}
|
|
827
|
+
finally {
|
|
828
|
+
ac.signal.removeEventListener("abort", onAbort);
|
|
829
|
+
clearIdle();
|
|
830
|
+
}
|
|
824
831
|
if (aborted || ac.signal.aborted) {
|
|
825
832
|
return {
|
|
826
833
|
content: [
|
|
@@ -1120,7 +1127,8 @@ export class GoalService {
|
|
|
1120
1127
|
const conv = this.host.activeConv();
|
|
1121
1128
|
conv.goalGeneration += 1;
|
|
1122
1129
|
// 委托执行:停掉在飞的执行者(循环靠代次作废 + 代次守卫退出)。
|
|
1123
|
-
|
|
1130
|
+
// await 落定,保证旧执行者已移出会话表,后续重设目标不再误报名额(issue #464)。
|
|
1131
|
+
await this.stopDelegated(conv);
|
|
1124
1132
|
this.resetLoopCounters(conv);
|
|
1125
1133
|
this.clearGoalFields(conv);
|
|
1126
1134
|
this.emitGoalStatus();
|
|
@@ -1154,14 +1162,15 @@ export class GoalService {
|
|
|
1154
1162
|
*/
|
|
1155
1163
|
async stopAllGoals() {
|
|
1156
1164
|
let stopped = 0;
|
|
1157
|
-
//
|
|
1158
|
-
|
|
1159
|
-
for (const convId of this.delegatedLoops.keys()) {
|
|
1165
|
+
// 快照迭代:await stopDelegated 期间,循环退出的回调可能增删这张表。
|
|
1166
|
+
for (const convId of [...this.delegatedLoops.keys()]) {
|
|
1160
1167
|
const conv = this.host.getConv(convId);
|
|
1161
1168
|
if (!conv)
|
|
1162
1169
|
continue;
|
|
1163
1170
|
conv.goalGeneration += 1; // 作废在飞回调(循环靠代次守卫退出)
|
|
1164
|
-
|
|
1171
|
+
// 逐个 await 停/收落定(stopDelegated 已按会话串行化,issue #464):
|
|
1172
|
+
// 停执行者 + 唤醒 verdict 等待者 + 清 roles/phase
|
|
1173
|
+
await this.stopDelegated(conv);
|
|
1165
1174
|
this.resetLoopCounters(conv);
|
|
1166
1175
|
this.clearGoalFields(conv);
|
|
1167
1176
|
stopped++;
|
|
@@ -1205,6 +1214,8 @@ export class GoalService {
|
|
|
1205
1214
|
}
|
|
1206
1215
|
conv.goalGeneration += 1;
|
|
1207
1216
|
// 委托执行:手动停止也要把在飞的执行者停掉(否则它还在后台改工作区)。
|
|
1217
|
+
// 本钩子是同步上下文,fire-and-forget 即可:stopDelegated 已按会话
|
|
1218
|
+
// 串行化(issue #464),重设/清除目标的一方会先等这次 dismiss 落定。
|
|
1208
1219
|
this.stopDelegated(conv);
|
|
1209
1220
|
this.resetLoopCounters(conv);
|
|
1210
1221
|
g.conversationId = null;
|
|
@@ -1398,7 +1409,24 @@ export class GoalService {
|
|
|
1398
1409
|
conv.goal.roles = {};
|
|
1399
1410
|
return exec?.convId;
|
|
1400
1411
|
}
|
|
1412
|
+
stopInflight = new Map();
|
|
1413
|
+
/**
|
|
1414
|
+
* 停掉委托执行者(stop + dismiss + 唤醒等待者)。同一会话并发调用时按序串行:
|
|
1415
|
+
* 上一次 stop/dismiss 未落定前,下一次先等它 —— 否则重设目标时旧执行者尚未
|
|
1416
|
+
* 移出会话表,随后 spawn 的名额检查会数到它而误报容量上限(issue #464)。
|
|
1417
|
+
* 同步上下文(onAgentEnd 等钩子)可以继续 fire-and-forget,串行化由这里兜住。
|
|
1418
|
+
*/
|
|
1401
1419
|
stopDelegated(conv) {
|
|
1420
|
+
const prev = this.stopInflight.get(conv.id);
|
|
1421
|
+
const run = prev ? prev.catch(() => { }).then(() => this.stopDelegatedNow(conv)) : this.stopDelegatedNow(conv);
|
|
1422
|
+
this.stopInflight.set(conv.id, run);
|
|
1423
|
+
void run.finally(() => {
|
|
1424
|
+
if (this.stopInflight.get(conv.id) === run)
|
|
1425
|
+
this.stopInflight.delete(conv.id);
|
|
1426
|
+
});
|
|
1427
|
+
return run;
|
|
1428
|
+
}
|
|
1429
|
+
async stopDelegatedNow(conv) {
|
|
1402
1430
|
// 排队的启动请求一并作废:setGoal 会在后面按新代次重新排,clearGoal 则不需要。
|
|
1403
1431
|
this.delegatedPending.delete(conv.id);
|
|
1404
1432
|
conv.awaitingVerdict = undefined;
|
|
@@ -1411,6 +1439,30 @@ export class GoalService {
|
|
|
1411
1439
|
settle("gone");
|
|
1412
1440
|
}
|
|
1413
1441
|
const execId = this.detachRoleExec(conv);
|
|
1442
|
+
if (execId) {
|
|
1443
|
+
try {
|
|
1444
|
+
await this.host.stopRoleAgent?.(execId);
|
|
1445
|
+
}
|
|
1446
|
+
catch { }
|
|
1447
|
+
try {
|
|
1448
|
+
// issue #464: 必须等待执行者完全 dismiss 移出会话表,避免立即重设目标时误报容量超限
|
|
1449
|
+
await this.host.dismissRoleAgent?.(execId);
|
|
1450
|
+
}
|
|
1451
|
+
catch { }
|
|
1452
|
+
}
|
|
1453
|
+
conv.goal.phase = "idle";
|
|
1454
|
+
}
|
|
1455
|
+
/** 会话过户时清理目标审查态并唤醒等待者(issue #457)。 */
|
|
1456
|
+
notifyTakeover(conv) {
|
|
1457
|
+
this.delegatedPending.delete(conv.id);
|
|
1458
|
+
conv.awaitingVerdict = undefined;
|
|
1459
|
+
void this.flushDeferredPrompts(conv);
|
|
1460
|
+
const settle = this.verdictWaiters.get(conv.id);
|
|
1461
|
+
if (settle) {
|
|
1462
|
+
this.verdictWaiters.delete(conv.id);
|
|
1463
|
+
settle("gone");
|
|
1464
|
+
}
|
|
1465
|
+
const execId = this.detachRoleExec(conv);
|
|
1414
1466
|
if (execId) {
|
|
1415
1467
|
void this.host.stopRoleAgent?.(execId).catch(() => { });
|
|
1416
1468
|
void this.host.dismissRoleAgent?.(execId).catch(() => { });
|
|
@@ -1581,6 +1633,14 @@ export class GoalService {
|
|
|
1581
1633
|
// 落盘对话在左栏/历史里没有「子代理」微标,用带前缀的标题保持可辨识。
|
|
1582
1634
|
title: this.roleConvTitle(goalText),
|
|
1583
1635
|
});
|
|
1636
|
+
// #490/案1:spawn 的长 await 期间目标可能被重设/清除(goalGeneration 已变,
|
|
1637
|
+
// stopDelegated 当时无可停执行者)——老循环不得把带着老目标首轮 prompt 的
|
|
1638
|
+
// 执行者写回 conv.roleExec,否则新循环会把它当现役执行者 steer(上下文被
|
|
1639
|
+
// 老目标污染,新目标首轮产出混入老目标工作)。就地停掉并让老循环按代次退出。
|
|
1640
|
+
if (conv.goalGeneration !== goalGeneration) {
|
|
1641
|
+
void host.stopRoleAgent?.(convId).catch(() => { });
|
|
1642
|
+
return undefined;
|
|
1643
|
+
}
|
|
1584
1644
|
conv.roleExec = { convId, generation: (conv.roleExec?.generation ?? 0) + 1 };
|
|
1585
1645
|
g.roles = { ...g.roles, executor: { convId, spawned: true } };
|
|
1586
1646
|
this.beginRoundVitals(conv, convId);
|
|
@@ -1762,6 +1822,13 @@ export class GoalService {
|
|
|
1762
1822
|
phase: "start",
|
|
1763
1823
|
round,
|
|
1764
1824
|
});
|
|
1825
|
+
// #490/案2:pushReviewCard 的 await 窗口里 setGoal/clearGoal 可能已清掉
|
|
1826
|
+
// awaitingVerdict(此刻 verdictWaiters 尚未登记,stopDelegated 的 settle 落空)
|
|
1827
|
+
// ——不复检就会继续登记 waiter 并把老目标的审查指令投进主对话:主对话白烧
|
|
1828
|
+
// 一轮审查、老循环干等 20 分钟超时,新目标的 pending 循环全程无法启动。
|
|
1829
|
+
if (!conv.awaitingVerdict || conv.awaitingVerdict.round !== round) {
|
|
1830
|
+
return "gone";
|
|
1831
|
+
}
|
|
1765
1832
|
const verdict = await new Promise((resolve) => {
|
|
1766
1833
|
let settled = false;
|
|
1767
1834
|
let timer;
|
package/dist/server/index.js
CHANGED
|
@@ -56,7 +56,7 @@ import { syncPluginCatalog } from "./plugin-catalog-sync.js";
|
|
|
56
56
|
import { McpBridge } from "./mcp-bridge.js";
|
|
57
57
|
import { createMcpHotReload } from "./mcp-hot-reload.js";
|
|
58
58
|
import { createHostMetricsSampler } from "./host-metrics.js";
|
|
59
|
-
import { SchedulerStore } from "./scheduler-tasks.js";
|
|
59
|
+
import { SchedulerStore, SchedulerValidationError } from "./scheduler-tasks.js";
|
|
60
60
|
import { initHttpProxy } from "./http-proxy.js";
|
|
61
61
|
import { globalLspPool } from "./lsp-tool.js";
|
|
62
62
|
import { buildPiWebTokenCookie, decodeCookieToken, isTlsRequest } from "./auth-cookie.js";
|
|
@@ -89,6 +89,13 @@ function cliFlag(name) {
|
|
|
89
89
|
return undefined;
|
|
90
90
|
}
|
|
91
91
|
const PORT = Number(cliFlag("--port") ?? process.env.PI_WEB_PORT ?? 8787);
|
|
92
|
+
// #506:直启路径与 bin 层对齐做端口校验——`--port abc` / `PI_WEB_PORT=abc` 之前会
|
|
93
|
+
// listen(NaN) 被 Node 当 0 绑到随机端口,且日志打印 http://localhost:NaN。
|
|
94
|
+
if (!Number.isInteger(PORT) || PORT <= 0 || PORT > 65535) {
|
|
95
|
+
console.error(`无效端口: ${cliFlag("--port") ?? process.env.PI_WEB_PORT} (--port / PI_WEB_PORT)`);
|
|
96
|
+
console.error(`Invalid port: ${cliFlag("--port") ?? process.env.PI_WEB_PORT} (--port / PI_WEB_PORT)`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
92
99
|
const CWD = resolve(cliFlag("--cwd") ?? process.env.PI_WEB_CWD ?? process.cwd());
|
|
93
100
|
const DATA_DIR = resolve(cliFlag("--data-dir") ?? process.env.PI_WEB_DATA_DIR ?? join(homedir(), ".pi-web"));
|
|
94
101
|
// The data dir is where the control socket, client state, plugins, themes and
|
|
@@ -976,6 +983,8 @@ httpServer.on("upgrade", (req, socket, head) => {
|
|
|
976
983
|
// closing sockets, sleep/wake, network partitions). Also broadcasts lightweight
|
|
977
984
|
// server host metrics (CPU and memory usage) sampled every ~2s.
|
|
978
985
|
const HEARTBEAT_INTERVAL_MS = 2_000;
|
|
986
|
+
/** 全局广播与心跳推送背压阈值(256KB):socket 积压时丢弃广播消息,防用户态无界堆积(issue #460)。 */
|
|
987
|
+
const BROADCAST_BACKPRESSURE_MAX_BYTES = 262_144;
|
|
979
988
|
const sampleHostMetrics = createHostMetricsSampler();
|
|
980
989
|
const heartbeatTimer = setInterval(() => {
|
|
981
990
|
let message = { type: "heartbeat" };
|
|
@@ -988,8 +997,40 @@ const heartbeatTimer = setInterval(() => {
|
|
|
988
997
|
if (wss.clients.size > 0) {
|
|
989
998
|
const payload = JSON.stringify(message);
|
|
990
999
|
for (const ws of wss.clients) {
|
|
991
|
-
if (ws.readyState
|
|
992
|
-
|
|
1000
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
1001
|
+
continue;
|
|
1002
|
+
const tracked = ws;
|
|
1003
|
+
// 半开连接死连接回收(issue #460):连续无活动/无 pong 则 terminate
|
|
1004
|
+
if (tracked.isAlive === false) {
|
|
1005
|
+
tracked.missedPings = (tracked.missedPings || 0) + 1;
|
|
1006
|
+
if (tracked.missedPings >= 3) {
|
|
1007
|
+
try {
|
|
1008
|
+
ws.terminate?.();
|
|
1009
|
+
}
|
|
1010
|
+
catch { }
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
tracked.missedPings = 0;
|
|
1016
|
+
}
|
|
1017
|
+
tracked.isAlive = false;
|
|
1018
|
+
try {
|
|
1019
|
+
ws.ping();
|
|
1020
|
+
}
|
|
1021
|
+
catch {
|
|
1022
|
+
try {
|
|
1023
|
+
ws.terminate?.();
|
|
1024
|
+
}
|
|
1025
|
+
catch { }
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
// 广播背压:缓冲积压严重时跳过心跳推送
|
|
1029
|
+
if (ws.bufferedAmount <= BROADCAST_BACKPRESSURE_MAX_BYTES) {
|
|
1030
|
+
try {
|
|
1031
|
+
ws.send(payload);
|
|
1032
|
+
}
|
|
1033
|
+
catch { }
|
|
993
1034
|
}
|
|
994
1035
|
}
|
|
995
1036
|
}
|
|
@@ -1272,6 +1313,8 @@ function pushSchedulerTasks() {
|
|
|
1272
1313
|
for (const client of wss.clients) {
|
|
1273
1314
|
if (client.readyState !== WebSocket.OPEN)
|
|
1274
1315
|
continue;
|
|
1316
|
+
if (client.bufferedAmount > BROADCAST_BACKPRESSURE_MAX_BYTES)
|
|
1317
|
+
continue;
|
|
1275
1318
|
try {
|
|
1276
1319
|
client.send(payload);
|
|
1277
1320
|
}
|
|
@@ -1290,6 +1333,8 @@ function pushNoticeToAll(level, text, textEn) {
|
|
|
1290
1333
|
for (const client of wss.clients) {
|
|
1291
1334
|
if (client.readyState !== WebSocket.OPEN)
|
|
1292
1335
|
continue;
|
|
1336
|
+
if (client.bufferedAmount > BROADCAST_BACKPRESSURE_MAX_BYTES)
|
|
1337
|
+
continue;
|
|
1293
1338
|
try {
|
|
1294
1339
|
client.send(payload);
|
|
1295
1340
|
}
|
|
@@ -1535,6 +1580,13 @@ function serializeShared(msg) {
|
|
|
1535
1580
|
return s;
|
|
1536
1581
|
}
|
|
1537
1582
|
wss.on("connection", (ws) => {
|
|
1583
|
+
const tracked = ws;
|
|
1584
|
+
tracked.isAlive = true;
|
|
1585
|
+
tracked.missedPings = 0;
|
|
1586
|
+
ws.on("pong", () => {
|
|
1587
|
+
tracked.isAlive = true;
|
|
1588
|
+
tracked.missedPings = 0;
|
|
1589
|
+
});
|
|
1538
1590
|
// Count attached sockets (the control socket reports REAL sockets, not
|
|
1539
1591
|
// cached client-session objects).
|
|
1540
1592
|
service.noteSocketOpen();
|
|
@@ -1651,7 +1703,9 @@ wss.on("connection", (ws) => {
|
|
|
1651
1703
|
break;
|
|
1652
1704
|
}
|
|
1653
1705
|
case "queue_remove":
|
|
1654
|
-
|
|
1706
|
+
// #491:removeQueued 是 async——dispatch 是 fire-and-forget,缺 void 时
|
|
1707
|
+
// 内部抛错即 unhandledRejection,而全仓没有兜底 handler,Node ≥15 直接崩进程。
|
|
1708
|
+
void cs.removeQueued(msg.kind, msg.text, msg.index);
|
|
1655
1709
|
break;
|
|
1656
1710
|
case "draft_update":
|
|
1657
1711
|
cs.saveDraft?.(msg.sessionId, msg.text, msg.ts);
|
|
@@ -1686,12 +1740,6 @@ wss.on("connection", (ws) => {
|
|
|
1686
1740
|
case "rollback_session":
|
|
1687
1741
|
void cs.rollbackSession?.(msg.messageId, msg.conversationId, msg.restoreWorkspace);
|
|
1688
1742
|
break;
|
|
1689
|
-
case "cycle_model":
|
|
1690
|
-
void cs.cycleModel();
|
|
1691
|
-
break;
|
|
1692
|
-
case "cycle_thinking":
|
|
1693
|
-
cs.cycleThinking();
|
|
1694
|
-
break;
|
|
1695
1743
|
case "get_state":
|
|
1696
1744
|
// Always a FULL snapshot: the client is (re)connecting or detected
|
|
1697
1745
|
// a rev/seq gap — it needs an authoritative state to rebuild from.
|
|
@@ -1745,12 +1793,14 @@ wss.on("connection", (ws) => {
|
|
|
1745
1793
|
void cs.setConversationPinned(msg.id, msg.pinned);
|
|
1746
1794
|
}
|
|
1747
1795
|
break;
|
|
1796
|
+
case "pin_session":
|
|
1797
|
+
if (typeof cs.pinSession === "function") {
|
|
1798
|
+
void cs.pinSession(msg.path, msg.pinned);
|
|
1799
|
+
}
|
|
1800
|
+
break;
|
|
1748
1801
|
case "dismiss_finished_subagents":
|
|
1749
1802
|
void cs.dismissFinishedSubagents(msg.parentId);
|
|
1750
1803
|
break;
|
|
1751
|
-
case "subagent_handoff":
|
|
1752
|
-
void cs.handoffSubagent?.(msg.fromRunId, msg.toRunId, msg.payload);
|
|
1753
|
-
break;
|
|
1754
1804
|
case "switch_session":
|
|
1755
1805
|
void cs.switchSession(msg.path);
|
|
1756
1806
|
break;
|
|
@@ -2231,6 +2281,9 @@ wss.on("connection", (ws) => {
|
|
|
2231
2281
|
done: async (ok, info) => {
|
|
2232
2282
|
if (ok) {
|
|
2233
2283
|
await reloadPluginsAndPush(jobLang);
|
|
2284
|
+
if (typeof cs?.checkPluginUpdates === "function") {
|
|
2285
|
+
void cs.checkPluginUpdates(false);
|
|
2286
|
+
}
|
|
2234
2287
|
const isZh = jobLang() === "zh";
|
|
2235
2288
|
const actionLabel = jobAction === "uninstall"
|
|
2236
2289
|
? isZh
|
|
@@ -2472,6 +2525,18 @@ wss.on("connection", (ws) => {
|
|
|
2472
2525
|
case "plan_update":
|
|
2473
2526
|
cs.updatePlan?.(msg.steps, msg.activeStepId, msg.conversationId);
|
|
2474
2527
|
break;
|
|
2528
|
+
case "plan_step_update":
|
|
2529
|
+
cs.updatePlanStep?.(msg.stepId, msg.patch, msg.conversationId);
|
|
2530
|
+
break;
|
|
2531
|
+
case "plan_step_delete":
|
|
2532
|
+
cs.deletePlanStep?.(msg.stepId, msg.conversationId);
|
|
2533
|
+
break;
|
|
2534
|
+
case "plan_step_add":
|
|
2535
|
+
cs.addPlanStep?.(msg.step, msg.afterStepId, msg.conversationId);
|
|
2536
|
+
break;
|
|
2537
|
+
case "plan_clean_handoff":
|
|
2538
|
+
void cs.planCleanHandoff?.(msg.steps, msg.prompt);
|
|
2539
|
+
break;
|
|
2475
2540
|
case "set_plan_mode":
|
|
2476
2541
|
// 计划模式(只规划不实施):会话级开关,热生效(提示词重建 + 工具硬闸门)。
|
|
2477
2542
|
void cs.setPlanMode?.(msg.enabled, msg.conversationId);
|
|
@@ -2532,11 +2597,12 @@ wss.on("connection", (ws) => {
|
|
|
2532
2597
|
scheduler.upsert(msg.task);
|
|
2533
2598
|
}
|
|
2534
2599
|
catch (err) {
|
|
2600
|
+
const isVal = err instanceof SchedulerValidationError;
|
|
2535
2601
|
send({
|
|
2536
2602
|
type: "notice",
|
|
2537
2603
|
level: "error",
|
|
2538
|
-
text: `保存定时任务失败:${err.message}`,
|
|
2539
|
-
textEn: `Failed to save scheduled task: ${err.message}`,
|
|
2604
|
+
text: `保存定时任务失败:${isVal ? err.messageZh : err.message}`,
|
|
2605
|
+
textEn: `Failed to save scheduled task: ${isVal ? err.messageEn : err.message}`,
|
|
2540
2606
|
});
|
|
2541
2607
|
}
|
|
2542
2608
|
break;
|
|
@@ -2600,6 +2666,8 @@ wss.on("connection", (ws) => {
|
|
|
2600
2666
|
service: SERVICE_INFO ?? undefined,
|
|
2601
2667
|
});
|
|
2602
2668
|
ws.on("message", (data) => {
|
|
2669
|
+
tracked.isAlive = true;
|
|
2670
|
+
tracked.missedPings = 0;
|
|
2603
2671
|
let msg;
|
|
2604
2672
|
try {
|
|
2605
2673
|
msg = JSON.parse(data.toString());
|
|
@@ -136,10 +136,6 @@ export function launchOrigin() {
|
|
|
136
136
|
cached ??= detectLaunchOrigin();
|
|
137
137
|
return cached;
|
|
138
138
|
}
|
|
139
|
-
/** 仅测试用:清掉缓存。 */
|
|
140
|
-
export function resetLaunchOriginCache() {
|
|
141
|
-
cached = null;
|
|
142
|
-
}
|
|
143
139
|
/** 下发给浏览器的服务信息(null = 没有 supervisor,界面不提供「重启服务」)。 */
|
|
144
140
|
export function toServiceInfo(origin) {
|
|
145
141
|
if (!origin.supervisor)
|
package/dist/server/locales.js
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
* tolerable. Manually dropped <dataDir>/locales/*.json files work too
|
|
14
14
|
* (offline installs) — the list is read from disk.
|
|
15
15
|
*/
|
|
16
|
-
import { existsSync,
|
|
16
|
+
import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { extractServerStrings, registerServerStrings, unregisterServerStrings } from "./i18n.js";
|
|
19
|
+
import { writeJsonAtomicSync } from "./atomic-file.js";
|
|
19
20
|
/** Packs available for download (alphabetical by code). */
|
|
20
21
|
export const LOCALE_PACKS = [
|
|
21
22
|
{ code: "de", nativeName: "Deutsch" },
|
|
@@ -188,11 +189,8 @@ export async function installPack(dataDir, code, opts = {}) {
|
|
|
188
189
|
const v = validatePack(data, code);
|
|
189
190
|
if (!v.ok)
|
|
190
191
|
throw new Error(`invalid pack ${code}: ${v.error}`);
|
|
191
|
-
mkdirSync(join(dataDir, "locales"), { recursive: true });
|
|
192
192
|
const file = packPath(dataDir, code);
|
|
193
|
-
|
|
194
|
-
writeFileSync(tmp, JSON.stringify(v.pack));
|
|
195
|
-
renameSync(tmp, file);
|
|
193
|
+
writeJsonAtomicSync(file, v.pack, 0);
|
|
196
194
|
return { code, nativeName: v.pack.nativeName, version: v.pack.version };
|
|
197
195
|
}
|
|
198
196
|
catch (e) {
|
package/dist/server/lsp-tool.js
CHANGED
|
@@ -19,6 +19,7 @@ import { delimiter, extname, isAbsolute, join, relative, resolve, sep } from "no
|
|
|
19
19
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
20
20
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { Type } from "typebox";
|
|
22
|
+
import { pick } from "./i18n.js";
|
|
22
23
|
export const LSP_TOOL_NAME = "lsp";
|
|
23
24
|
export const LSP_SYMBOL_KINDS = {
|
|
24
25
|
1: "File",
|
|
@@ -694,13 +695,22 @@ export async function getLiveLspDiagnostics(absPath, cwd) {
|
|
|
694
695
|
});
|
|
695
696
|
return `⚠️ Post-edit Diagnostics (${errors.length} error${errors.length > 1 ? "s" : ""}):\n${lines.join("\n")}`;
|
|
696
697
|
}
|
|
698
|
+
/** 文本大纲与 details.symbols 共用的展平符号数上限(顶层与子级一并计数)。 */
|
|
699
|
+
const DOCUMENT_SYMBOL_OUTLINE_CAP = 300;
|
|
700
|
+
/**
|
|
701
|
+
* details.symbols 的序列化体积预算(字符数)。details 随会话持久且整体 ≤64KB,
|
|
702
|
+
* 超限会被整条丢弃(见 serialize.ts 的 TOOL_DETAILS_CAP);符号名/签名极长的
|
|
703
|
+
* 极端文件(生成的 .d.ts、protobuf 产物)下 300 个符号也可能撑爆,故按实际
|
|
704
|
+
* 序列化体积逐步收紧上限(预算留足余量给 count/truncated 等包装字段)。
|
|
705
|
+
*/
|
|
706
|
+
const DOCUMENT_SYMBOL_DETAILS_CHAR_BUDGET = 60_000;
|
|
697
707
|
/**
|
|
698
708
|
* 递归格式化 DocumentSymbol 列表为缩进的符号大纲树(做条数上限保护)
|
|
699
709
|
*/
|
|
700
710
|
function formatDocumentSymbols(symbols, indent = "", lines = []) {
|
|
701
711
|
for (const sym of symbols) {
|
|
702
|
-
if (lines.length >=
|
|
703
|
-
lines.push(`${indent}• ... [Truncated: outline exceeds
|
|
712
|
+
if (lines.length >= DOCUMENT_SYMBOL_OUTLINE_CAP) {
|
|
713
|
+
lines.push(`${indent}• ... [Truncated: outline exceeds ${DOCUMENT_SYMBOL_OUTLINE_CAP} symbols]`);
|
|
704
714
|
break;
|
|
705
715
|
}
|
|
706
716
|
const kind = LSP_SYMBOL_KINDS[sym.kind] || `Kind(${sym.kind})`;
|
|
@@ -716,6 +726,56 @@ function formatDocumentSymbols(symbols, indent = "", lines = []) {
|
|
|
716
726
|
}
|
|
717
727
|
return lines;
|
|
718
728
|
}
|
|
729
|
+
/**
|
|
730
|
+
* 与 formatDocumentSymbols 同口径的结构化裁剪:按展平后的符号数(先序遍历,
|
|
731
|
+
* 顶层与子级一并计数)截断到 cap 个。只在展平序列的边界上裁剪,保留符号的
|
|
732
|
+
* 父子结构保持一致 —— 父被裁掉则整棵子树不再出现;有符号被裁时 truncated 置真。
|
|
733
|
+
* 未触发截断时原样返回(不拷贝,避免大对象无谓重建)。
|
|
734
|
+
*/
|
|
735
|
+
function capDocumentSymbolTree(symbols, cap) {
|
|
736
|
+
let count = 0;
|
|
737
|
+
let truncated = false;
|
|
738
|
+
const walk = (list) => {
|
|
739
|
+
const kept = [];
|
|
740
|
+
let changed = false;
|
|
741
|
+
for (const sym of list) {
|
|
742
|
+
if (count >= cap) {
|
|
743
|
+
truncated = true;
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
count += 1;
|
|
747
|
+
if (Array.isArray(sym.children) && sym.children.length > 0) {
|
|
748
|
+
const sub = walk(sym.children);
|
|
749
|
+
if (sub.changed || sub.kept.length !== sym.children.length) {
|
|
750
|
+
changed = true;
|
|
751
|
+
kept.push({ ...sym, children: sub.kept });
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
kept.push(sym);
|
|
756
|
+
}
|
|
757
|
+
return { kept, changed };
|
|
758
|
+
};
|
|
759
|
+
const { kept } = walk(symbols);
|
|
760
|
+
return { capped: truncated ? kept : symbols, truncated };
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* details.symbols 的最终裁剪:先与文本大纲同口径按展平计数截到 300;再用实际
|
|
764
|
+
* 序列化体积校验 —— details 随会话持久且整体 ≤64KB,超限整条丢弃(见
|
|
765
|
+
* serialize.ts 的 TOOL_DETAILS_CAP),符号名/签名极长的极端文件下按半数逐步
|
|
766
|
+
* 收紧展平上限,保证结构化大纲真的能进快照而不是被整条丢掉。
|
|
767
|
+
*/
|
|
768
|
+
function capDocumentSymbolsForDetails(symbols) {
|
|
769
|
+
let cap = DOCUMENT_SYMBOL_OUTLINE_CAP;
|
|
770
|
+
let res = capDocumentSymbolTree(symbols, cap);
|
|
771
|
+
let size = JSON.stringify(res.capped).length;
|
|
772
|
+
while (size > DOCUMENT_SYMBOL_DETAILS_CHAR_BUDGET && cap > 1) {
|
|
773
|
+
cap = Math.floor(cap / 2);
|
|
774
|
+
res = capDocumentSymbolTree(symbols, cap);
|
|
775
|
+
size = JSON.stringify(res.capped).length;
|
|
776
|
+
}
|
|
777
|
+
return res;
|
|
778
|
+
}
|
|
719
779
|
/**
|
|
720
780
|
* 递归单趟查找符号(两遍扫描:先严格精确匹配,未命中再执行大小写忽略回退,避免遮蔽后续精确符号;支持 containerName 点分路径)
|
|
721
781
|
*/
|
|
@@ -793,6 +853,7 @@ function findDefaultSourceFileForLsp(cwd) {
|
|
|
793
853
|
}
|
|
794
854
|
export function makeLspTool(options) {
|
|
795
855
|
const cwd = options.cwd;
|
|
856
|
+
const getLang = options.lang ?? (() => "en");
|
|
796
857
|
return defineTool({
|
|
797
858
|
name: LSP_TOOL_NAME,
|
|
798
859
|
promptSnippet: "IDE-grade semantic analysis (LSP) across the workspace: definition, references, hover, diagnostics, symbols, impact check",
|
|
@@ -838,13 +899,16 @@ Lines are 1-indexed.`,
|
|
|
838
899
|
description: "1-indexed column/character position (defaults to 1).",
|
|
839
900
|
})),
|
|
840
901
|
timeout: Type.Optional(Type.Number({
|
|
841
|
-
|
|
902
|
+
minimum: 1,
|
|
903
|
+
maximum: 120,
|
|
904
|
+
description: "Timeout in seconds (1-120, defaults to 15).",
|
|
842
905
|
})),
|
|
843
906
|
allowInstall: Type.Optional(Type.Boolean({
|
|
844
907
|
description: "Install the missing language server into ~/.pi-web/lsp-servers (user-space, no sudo). Default false: the tool returns an installHint instead.",
|
|
845
908
|
})),
|
|
846
909
|
}),
|
|
847
910
|
async execute(_callId, params, _signal, _onUpdate, _ctx) {
|
|
911
|
+
const L = getLang();
|
|
848
912
|
const action = params.action;
|
|
849
913
|
let targetPath = params.path;
|
|
850
914
|
if (!targetPath && action === "workspaceSymbol") {
|
|
@@ -854,7 +918,7 @@ Lines are 1-indexed.`,
|
|
|
854
918
|
content: [
|
|
855
919
|
{
|
|
856
920
|
type: "text",
|
|
857
|
-
text:
|
|
921
|
+
text: pick(L, "错误:无法自动检测项目主要源码文件以路由语言服务器。请提供 'path' 参数(指向项目中任意源码文件,例如 path='src/index.ts')。", "Error: Could not automatically detect a primary project source file to route language server. Please provide 'path' (pointing to any source file in the project, e.g. path='src/index.ts') to select the language server."),
|
|
858
922
|
},
|
|
859
923
|
],
|
|
860
924
|
details: { ok: false, error: "Missing path: cannot route language server" },
|
|
@@ -863,14 +927,19 @@ Lines are 1-indexed.`,
|
|
|
863
927
|
}
|
|
864
928
|
if (!targetPath) {
|
|
865
929
|
return {
|
|
866
|
-
content: [
|
|
930
|
+
content: [
|
|
931
|
+
{
|
|
932
|
+
type: "text",
|
|
933
|
+
text: pick(L, `错误:执行操作 '${action}' 时必须提供 'path' 参数。`, `Error: 'path' parameter is required for action '${action}'.`),
|
|
934
|
+
},
|
|
935
|
+
],
|
|
867
936
|
details: { ok: false, error: "Missing path parameter" },
|
|
868
937
|
};
|
|
869
938
|
}
|
|
870
939
|
const absPath = isAbsolute(targetPath) ? targetPath : resolve(cwd, targetPath);
|
|
871
940
|
const line = typeof params.line === "number" ? Math.max(1, params.line) : 1;
|
|
872
941
|
const character = typeof params.character === "number" ? Math.max(1, params.character) : 1;
|
|
873
|
-
const timeoutMs = (params.timeout ?? 15) * 1000;
|
|
942
|
+
const timeoutMs = Math.max(1000, Math.min(120_000, Math.floor((params.timeout ?? 15) * 1000)));
|
|
874
943
|
const rel = relative(cwd, absPath);
|
|
875
944
|
if (rel === ".." ||
|
|
876
945
|
rel.startsWith(".." + sep) ||
|
|
@@ -878,13 +947,23 @@ Lines are 1-indexed.`,
|
|
|
878
947
|
rel.startsWith("..\\") ||
|
|
879
948
|
isAbsolute(rel)) {
|
|
880
949
|
return {
|
|
881
|
-
content: [
|
|
950
|
+
content: [
|
|
951
|
+
{
|
|
952
|
+
type: "text",
|
|
953
|
+
text: pick(L, `错误:路径穿越已被拒绝:${targetPath} 超出了工作区范围。`, `Error: Path traversal denied: ${targetPath} is outside workspace.`),
|
|
954
|
+
},
|
|
955
|
+
],
|
|
882
956
|
details: { ok: false, error: "Path traversal denied" },
|
|
883
957
|
};
|
|
884
958
|
}
|
|
885
959
|
if (!existsSync(absPath)) {
|
|
886
960
|
return {
|
|
887
|
-
content: [
|
|
961
|
+
content: [
|
|
962
|
+
{
|
|
963
|
+
type: "text",
|
|
964
|
+
text: pick(L, `错误:文件不存在:${targetPath}`, `Error: File not found: ${targetPath}`),
|
|
965
|
+
},
|
|
966
|
+
],
|
|
888
967
|
details: { ok: false, error: "File not found" },
|
|
889
968
|
};
|
|
890
969
|
}
|
|
@@ -893,7 +972,7 @@ Lines are 1-indexed.`,
|
|
|
893
972
|
content: [
|
|
894
973
|
{
|
|
895
974
|
type: "text",
|
|
896
|
-
text: `Error: 'symbol' parameter is required for 'read_symbol' action (e.g. symbol="parseConfig" or "ClassName.methodName")
|
|
975
|
+
text: pick(L, `错误:执行 'read_symbol' 操作必须提供 'symbol' 参数(例如 symbol="parseConfig" 或 "ClassName.methodName")。`, `Error: 'symbol' parameter is required for 'read_symbol' action (e.g. symbol="parseConfig" or "ClassName.methodName").`),
|
|
897
976
|
},
|
|
898
977
|
],
|
|
899
978
|
details: { ok: false, error: "Missing symbol parameter" },
|
|
@@ -904,7 +983,7 @@ Lines are 1-indexed.`,
|
|
|
904
983
|
content: [
|
|
905
984
|
{
|
|
906
985
|
type: "text",
|
|
907
|
-
text: `Error: 'query' parameter cannot be empty for 'workspaceSymbol' action. Please provide a search term (e.g. query='User' or 'Router')
|
|
986
|
+
text: pick(L, `错误:执行 'workspaceSymbol' 操作时 'query' 参数不能为空。请提供搜索词(例如 query='User' 或 'Router')。`, `Error: 'query' parameter cannot be empty for 'workspaceSymbol' action. Please provide a search term (e.g. query='User' or 'Router').`),
|
|
908
987
|
},
|
|
909
988
|
],
|
|
910
989
|
details: { ok: false, error: "Empty query parameter" },
|
|
@@ -1065,8 +1144,10 @@ Lines are 1-indexed.`,
|
|
|
1065
1144
|
};
|
|
1066
1145
|
}
|
|
1067
1146
|
const lines = formatDocumentSymbols(symbols);
|
|
1068
|
-
// details 随会话持久且整体 ≤64KB(超限整条丢弃):symbols
|
|
1069
|
-
|
|
1147
|
+
// details 随会话持久且整体 ≤64KB(超限整条丢弃):symbols 与文本大纲同口径
|
|
1148
|
+
// 截断 —— 按展平后的符号数(先序遍历,顶层与子级一并计数)截到 300,保留
|
|
1149
|
+
// 符号维持父子结构;再按序列化体积兜底收紧,极端文件下保证 details 进得了快照。
|
|
1150
|
+
const { capped: detailsSymbols, truncated: symbolsTruncated } = capDocumentSymbolsForDetails(symbols);
|
|
1070
1151
|
return {
|
|
1071
1152
|
content: [
|
|
1072
1153
|
{
|
|
@@ -1074,7 +1155,7 @@ Lines are 1-indexed.`,
|
|
|
1074
1155
|
text: `Symbols in ${targetPath} (${symbols.length} top-level):\n${lines.join("\n")}`,
|
|
1075
1156
|
},
|
|
1076
1157
|
],
|
|
1077
|
-
details: { ok: true, count: symbols.length, symbols: detailsSymbols },
|
|
1158
|
+
details: { ok: true, count: symbols.length, symbols: detailsSymbols, truncated: symbolsTruncated },
|
|
1078
1159
|
};
|
|
1079
1160
|
}
|
|
1080
1161
|
if (action === "read_symbol") {
|
|
@@ -1219,12 +1300,14 @@ Lines are 1-indexed.`,
|
|
|
1219
1300
|
const normalizePath = (p) => (process.platform === "win32" ? p.toLowerCase() : p);
|
|
1220
1301
|
// 1. 收集种子位置:给了 line/character 就只查那个符号;否则查全部顶层符号。
|
|
1221
1302
|
const seeds = [];
|
|
1303
|
+
let seedTotal = 0; // 顶层符号总数(截断前),超出 MAX_SEEDS 时在输出尾部标明
|
|
1222
1304
|
if (typeof params.line === "number") {
|
|
1223
1305
|
seeds.push({ line: line - 1, character: character - 1 });
|
|
1224
1306
|
}
|
|
1225
1307
|
else {
|
|
1226
1308
|
const symResult = await client.request("textDocument/documentSymbol", { textDocument: { uri } }, timeoutMs);
|
|
1227
1309
|
const topSymbols = Array.isArray(symResult) ? symResult : [];
|
|
1310
|
+
seedTotal = topSymbols.length;
|
|
1228
1311
|
for (const sym of topSymbols.slice(0, MAX_SEEDS)) {
|
|
1229
1312
|
const pos = sym.selectionRange?.start ?? sym.range?.start ?? sym.location?.range?.start;
|
|
1230
1313
|
if (pos && typeof pos.line === "number") {
|
|
@@ -1267,6 +1350,10 @@ Lines are 1-indexed.`,
|
|
|
1267
1350
|
depPaths.add(refPath);
|
|
1268
1351
|
}
|
|
1269
1352
|
}
|
|
1353
|
+
// 只分析排序后的前 MAX_DEPENDENTS 个引用方,但计数必须如实上报:文本与
|
|
1354
|
+
// details 都带截断前的真实总数,超出部分用尾注标明(对照 references 动作
|
|
1355
|
+
// 的 "... and N more references" 口径),避免模型误以为清单是完整的。
|
|
1356
|
+
const totalReferencing = depPaths.size;
|
|
1270
1357
|
const dependents = [...depPaths].sort().slice(0, MAX_DEPENDENTS);
|
|
1271
1358
|
if (dependents.length === 0) {
|
|
1272
1359
|
return {
|
|
@@ -1327,7 +1414,7 @@ Lines are 1-indexed.`,
|
|
|
1327
1414
|
}
|
|
1328
1415
|
impacted.sort((a, b) => b.errors - a.errors || b.warnings - a.warnings);
|
|
1329
1416
|
const out = [];
|
|
1330
|
-
out.push(`Impact cascade for ${targetPath}: ${
|
|
1417
|
+
out.push(`Impact cascade for ${targetPath}: ${totalReferencing} referencing file(s), ${impacted.length} with findings.`);
|
|
1331
1418
|
for (const item of impacted) {
|
|
1332
1419
|
out.push(`• ${item.path} — ${item.errors} error(s), ${item.warnings} warning(s)`);
|
|
1333
1420
|
for (const d of item.diagnostics.slice(0, 5)) {
|
|
@@ -1347,6 +1434,14 @@ Lines are 1-indexed.`,
|
|
|
1347
1434
|
if (notReported.length > 0) {
|
|
1348
1435
|
out.push(`Diagnostics not reported in time (${notReported.length}, server may still be analyzing): ${notReported.slice(0, 5).join(", ")}${notReported.length > 5 ? ", ..." : ""}`);
|
|
1349
1436
|
}
|
|
1437
|
+
// 截断尾注:与 references 动作的 "... and N more references" 同口径,
|
|
1438
|
+
// 让模型知道被丢弃的引用方/种子符号既不在 clean 也不在 notReported 里。
|
|
1439
|
+
if (totalReferencing > dependents.length) {
|
|
1440
|
+
out.push(`... and ${totalReferencing - dependents.length} more not analyzed`);
|
|
1441
|
+
}
|
|
1442
|
+
if (seedTotal > MAX_SEEDS) {
|
|
1443
|
+
out.push(`... and ${seedTotal - MAX_SEEDS} more top-level symbols not analyzed`);
|
|
1444
|
+
}
|
|
1350
1445
|
return {
|
|
1351
1446
|
content: [{ type: "text", text: out.join("\n") }],
|
|
1352
1447
|
details: {
|
|
@@ -1355,6 +1450,7 @@ Lines are 1-indexed.`,
|
|
|
1355
1450
|
clean,
|
|
1356
1451
|
notReported,
|
|
1357
1452
|
referencedFiles: dependents.map((p) => relative(cwd, p).replace(/\\/g, "/")),
|
|
1453
|
+
referencedFilesTotal: totalReferencing,
|
|
1358
1454
|
},
|
|
1359
1455
|
};
|
|
1360
1456
|
}
|