@trim21/personal-pi-extensions 0.1.511 → 0.1.513
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/package.json +1 -1
- package/src/aft/tools.ts +99 -6
- package/src/lib/lsp/lsp.ts +30 -7
- package/src/skills/lsp-config/SKILL.md +12 -15
package/package.json
CHANGED
package/src/aft/tools.ts
CHANGED
|
@@ -444,18 +444,20 @@ const semanticIndexProgressSchema = Type.Object({
|
|
|
444
444
|
|
|
445
445
|
type SemanticIndexProgress = Static<typeof semanticIndexProgressSchema>;
|
|
446
446
|
|
|
447
|
-
/**
|
|
448
|
-
|
|
447
|
+
/** 解析快照里的语义索引构建进度;非 Building 或形状不符返回 undefined(不显示)。 */
|
|
448
|
+
function parseSemanticIndexProgress(snapshot: StatusSnapshot): SemanticIndexProgress | undefined {
|
|
449
449
|
const raw = snapshot.semantic_index;
|
|
450
450
|
if (raw === undefined) {
|
|
451
451
|
return undefined;
|
|
452
452
|
}
|
|
453
|
-
let progress: SemanticIndexProgress;
|
|
454
453
|
try {
|
|
455
|
-
|
|
454
|
+
return Value.Parse(semanticIndexProgressSchema, raw);
|
|
456
455
|
} catch {
|
|
457
456
|
return undefined;
|
|
458
457
|
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function semanticIndexProgressParts(progress: SemanticIndexProgress): string[] {
|
|
459
461
|
const parts = [`语义索引构建中${progress.stage === undefined ? "" : ` (${progress.stage})`}`];
|
|
460
462
|
if (
|
|
461
463
|
progress.embedded_chunks !== undefined &&
|
|
@@ -475,7 +477,97 @@ export function formatSemanticIndexProgress(snapshot: StatusSnapshot): string |
|
|
|
475
477
|
) {
|
|
476
478
|
parts.push(`batch ${progress.current_batch}/${progress.total_batches}`);
|
|
477
479
|
}
|
|
478
|
-
return parts
|
|
480
|
+
return parts;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** status 快照 → 一行构建进度文本(不含 ETA)。 */
|
|
484
|
+
export function formatSemanticIndexProgress(snapshot: StatusSnapshot): string | undefined {
|
|
485
|
+
const progress = parseSemanticIndexProgress(snapshot);
|
|
486
|
+
if (progress === undefined) {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
return semanticIndexProgressParts(progress).join(" · ");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** eta 速率估计的滑动窗口:只看最近这段时间的样本,丢弃更早的。 */
|
|
493
|
+
const ETA_WINDOW_MS = 15_000;
|
|
494
|
+
/** 样本跨度过短时速率噪声太大(快照约每秒一条),不显示 ETA。 */
|
|
495
|
+
const ETA_MIN_WINDOW_MS = 3_000;
|
|
496
|
+
/** 滑动窗口里最多保留的样本数,防快照异常密集时无界增长。 */
|
|
497
|
+
const ETA_MAX_SAMPLES = 30;
|
|
498
|
+
|
|
499
|
+
interface EtaSample {
|
|
500
|
+
readonly at: number;
|
|
501
|
+
readonly embedded: number;
|
|
502
|
+
readonly stage: string | undefined;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function formatEta(ms: number): string {
|
|
506
|
+
// 取整到 5s 粒度,避免给出虚假的精确感。
|
|
507
|
+
const seconds = Math.max(5, Math.round(ms / 1000 / 5) * 5);
|
|
508
|
+
if (seconds < 60) {
|
|
509
|
+
return `${seconds}s`;
|
|
510
|
+
}
|
|
511
|
+
const minutes = Math.floor(seconds / 60);
|
|
512
|
+
const rest = seconds % 60;
|
|
513
|
+
return rest === 0 ? `${minutes}m` : `${minutes}m${rest}s`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* 带剩余时间估计的进度格式化器。基于快照序列做滑动窗口速率外推
|
|
518
|
+
* (embedding 批次速率受远端 API 波动影响并非线性,窗口越近越准)。
|
|
519
|
+
* total_chunks / total_batches 变化只影响分母,批次处理速率不变,
|
|
520
|
+
* 直接用新 total 重算 ETA,不清空样本;仅 stage 变化(换工作内容)或
|
|
521
|
+
* embedded 回退(watcher 重建)时重置。工厂 + 闭包持有状态:每次
|
|
522
|
+
* 工具调用 / CLI 运行创建独立实例。
|
|
523
|
+
*/
|
|
524
|
+
export function createSemanticIndexProgressFormatter(options?: {
|
|
525
|
+
now?: () => number;
|
|
526
|
+
}): (snapshot: StatusSnapshot) => string | undefined {
|
|
527
|
+
const now = options?.now ?? Date.now;
|
|
528
|
+
let samples: EtaSample[] = [];
|
|
529
|
+
|
|
530
|
+
return (snapshot: StatusSnapshot): string | undefined => {
|
|
531
|
+
const progress = parseSemanticIndexProgress(snapshot);
|
|
532
|
+
if (progress === undefined) {
|
|
533
|
+
return undefined;
|
|
534
|
+
}
|
|
535
|
+
const parts = semanticIndexProgressParts(progress);
|
|
536
|
+
|
|
537
|
+
const { embedded_chunks: embedded, total_chunks: total, stage } = progress;
|
|
538
|
+
const last = samples.at(-1);
|
|
539
|
+
if (last !== undefined && last.stage !== stage) {
|
|
540
|
+
samples = [];
|
|
541
|
+
}
|
|
542
|
+
if (embedded !== undefined && total !== undefined && total > 0) {
|
|
543
|
+
if (last !== undefined && embedded < last.embedded) {
|
|
544
|
+
samples = [];
|
|
545
|
+
}
|
|
546
|
+
const at = now();
|
|
547
|
+
samples.push({ at, embedded, stage });
|
|
548
|
+
samples = samples.filter((sample) => at - sample.at <= ETA_WINDOW_MS);
|
|
549
|
+
if (samples.length > ETA_MAX_SAMPLES) {
|
|
550
|
+
samples = samples.slice(-ETA_MAX_SAMPLES);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const first = samples.at(0);
|
|
555
|
+
const current = samples.at(-1);
|
|
556
|
+
if (
|
|
557
|
+
first !== undefined &&
|
|
558
|
+
current !== undefined &&
|
|
559
|
+
total !== undefined &&
|
|
560
|
+
first !== current &&
|
|
561
|
+
current.at - first.at >= ETA_MIN_WINDOW_MS
|
|
562
|
+
) {
|
|
563
|
+
const rate = (current.embedded - first.embedded) / (current.at - first.at);
|
|
564
|
+
const remaining = total - current.embedded;
|
|
565
|
+
if (rate > 0 && remaining > 0) {
|
|
566
|
+
parts.push(`剩余约 ${formatEta(remaining / rate)}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return parts.join(" · ");
|
|
570
|
+
};
|
|
479
571
|
}
|
|
480
572
|
|
|
481
573
|
const SearchParams = Type.Object(
|
|
@@ -520,11 +612,12 @@ export function registerSearchTool(pi: ExtensionAPI, ctx: AftToolContext): void
|
|
|
520
612
|
});
|
|
521
613
|
|
|
522
614
|
const bridge = bridgeFor(ctx);
|
|
615
|
+
const formatProgress = createSemanticIndexProgressFormatter();
|
|
523
616
|
const stopProgress =
|
|
524
617
|
onUpdate === undefined
|
|
525
618
|
? undefined
|
|
526
619
|
: subscribeBridgeStatus(bridge, (snapshot) => {
|
|
527
|
-
const text =
|
|
620
|
+
const text = formatProgress(snapshot);
|
|
528
621
|
if (text !== undefined) {
|
|
529
622
|
onUpdate({ content: [{ type: "text", text }], details: undefined });
|
|
530
623
|
}
|
package/src/lib/lsp/lsp.ts
CHANGED
|
@@ -425,9 +425,14 @@ export interface LspService {
|
|
|
425
425
|
start(): void;
|
|
426
426
|
/**
|
|
427
427
|
* 重启指定服务器:关闭其全部 client、清除对应失败记录并解除禁用;
|
|
428
|
-
*
|
|
428
|
+
* 其余服务器不受影响。配置缓存失效,下次工具调用按当前 cwd 重新读盘。
|
|
429
429
|
*/
|
|
430
430
|
reload(serverID: string): Promise<void>;
|
|
431
|
+
/**
|
|
432
|
+
* 重启全部服务器(/lsp-reload 无参):关闭所有 client、清空失败记录并
|
|
433
|
+
* 解除禁用;配置缓存失效,下次工具调用重新读盘。
|
|
434
|
+
*/
|
|
435
|
+
reloadAll(): Promise<void>;
|
|
431
436
|
/** 已知服务器 id(running 或 broken 的去重集合),供命令补全与提示。 */
|
|
432
437
|
serverIDs(): string[];
|
|
433
438
|
/** 注入 status 渲染回调;传入 undefined 表示不再渲染。 */
|
|
@@ -997,6 +1002,24 @@ export function createLspService(
|
|
|
997
1002
|
updateStatusText();
|
|
998
1003
|
}
|
|
999
1004
|
|
|
1005
|
+
async function reloadAll(): Promise<void> {
|
|
1006
|
+
state.closing = true;
|
|
1007
|
+
state.config = undefined;
|
|
1008
|
+
state.configCwd = undefined;
|
|
1009
|
+
await Promise.all(state.clients.map((client) => client.shutdown())).catch(() => {
|
|
1010
|
+
// 个别进程退出失败不阻止清理流程
|
|
1011
|
+
});
|
|
1012
|
+
state.clients = [];
|
|
1013
|
+
state.clientExtensions.clear();
|
|
1014
|
+
state.brokenFailAt.clear();
|
|
1015
|
+
state.brokenNotifiedAt.clear();
|
|
1016
|
+
state.servers.clear();
|
|
1017
|
+
await stopWatcher();
|
|
1018
|
+
state.closing = false;
|
|
1019
|
+
state.disabled = false;
|
|
1020
|
+
updateStatusText();
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1000
1023
|
function serverIDs(): string[] {
|
|
1001
1024
|
return [...new Set([...state.servers.values()].map((server) => server.serverID))];
|
|
1002
1025
|
}
|
|
@@ -1012,6 +1035,7 @@ export function createLspService(
|
|
|
1012
1035
|
stop,
|
|
1013
1036
|
start,
|
|
1014
1037
|
reload,
|
|
1038
|
+
reloadAll,
|
|
1015
1039
|
serverIDs,
|
|
1016
1040
|
attachStatus,
|
|
1017
1041
|
refreshStatus: updateStatusText,
|
|
@@ -1172,7 +1196,8 @@ export function createLspManager(
|
|
|
1172
1196
|
});
|
|
1173
1197
|
|
|
1174
1198
|
pi.registerCommand("lsp-reload", {
|
|
1175
|
-
description:
|
|
1199
|
+
description:
|
|
1200
|
+
"Restart LSP servers: /lsp-reload <server-id> for one, no argument reloads config and restarts all",
|
|
1176
1201
|
getArgumentCompletions: (prefix) =>
|
|
1177
1202
|
(service?.serverIDs() ?? [])
|
|
1178
1203
|
.toSorted()
|
|
@@ -1184,12 +1209,10 @@ export function createLspManager(
|
|
|
1184
1209
|
return;
|
|
1185
1210
|
}
|
|
1186
1211
|
const serverID = args.trim();
|
|
1187
|
-
const known = service.serverIDs().toSorted();
|
|
1188
1212
|
if (!serverID) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
);
|
|
1213
|
+
// 无参:重读配置并重启全部服务器
|
|
1214
|
+
await service.reloadAll();
|
|
1215
|
+
ctx.ui.notify("LSP reloaded: all servers will restart on the next tool call", "info");
|
|
1193
1216
|
return;
|
|
1194
1217
|
}
|
|
1195
1218
|
await service.reload(serverID);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lsp-config
|
|
3
|
-
description: Use when 编写、检查或排查 LSP 语言服务器配置 —— 项目本地 .pi/lsp.json 或全局 ~/.pi/agent/lsp.json:配置 typescript-language-server / pyright / ruff / gopls / clangd 等服务器的 bin、include、
|
|
3
|
+
description: Use when 编写、检查或排查 LSP 语言服务器配置 —— 项目本地 .pi/lsp.json 或全局 ~/.pi/agent/lsp.json:配置 typescript-language-server / pyright / ruff / gopls / clangd 等服务器的 bin、include、workingDir、initializationOptions、tsserver.path、watch 与诊断超时;或排查服务器起不来(binary not found / provides no tsserver.js)、typescript alias 依赖下 tsserver 找不到、诊断一直为空等问题。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 配置 LSP 服务器(.pi/lsp.json)
|
|
@@ -14,14 +14,14 @@ description: Use when 编写、检查或排查 LSP 语言服务器配置 ——
|
|
|
14
14
|
|
|
15
15
|
合并是**纯函数**(`mergeConfig(global, local)`),规则:
|
|
16
16
|
|
|
17
|
-
| 段 | 合并方式
|
|
18
|
-
| -------------------- |
|
|
19
|
-
| 顶层其余字段 | 本地覆盖全局(浅合并)
|
|
20
|
-
| `servers` | 按服务器 id **整条覆盖**:本地写某个 id 会替换全局同 id 的整条记录,**不是字段级合并**。只改一个字段(如补 `initializationOptions`)也必须把 `include`/`
|
|
21
|
-
| `watch` | 字段级合并:全局为基底、本地逐字段覆盖;`ignore` 两侧**并集去重**(全局在前),本地写 watch 段不会清掉全局 ignore
|
|
22
|
-
| 某段全局本地都未出现 | 保持缺失(调用方用 `...(watch && { watch })` 省略键,不要输出空对象)
|
|
17
|
+
| 段 | 合并方式 |
|
|
18
|
+
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| 顶层其余字段 | 本地覆盖全局(浅合并) |
|
|
20
|
+
| `servers` | 按服务器 id **整条覆盖**:本地写某个 id 会替换全局同 id 的整条记录,**不是字段级合并**。只改一个字段(如补 `initializationOptions`)也必须把 `include`/`workingDir`/`bin`/`args`/`languageIdByExtension` 全部带上,否则丢全局字段 |
|
|
21
|
+
| `watch` | 字段级合并:全局为基底、本地逐字段覆盖;`ignore` 两侧**并集去重**(全局在前),本地写 watch 段不会清掉全局 ignore |
|
|
22
|
+
| 某段全局本地都未出现 | 保持缺失(调用方用 `...(watch && { watch })` 省略键,不要输出空对象) |
|
|
23
23
|
|
|
24
|
-
配置经 typebox
|
|
24
|
+
配置经 typebox 校验:字段类型不符、非法时长格式等在读取时**直接抛错**;schema 外的未知字段(含已删除的遗留字段如 `rootMarkers`)**不拒绝**,以 warning notify 逐个上报。`version` 当前为 1。
|
|
25
25
|
|
|
26
26
|
## 顶层字段速查
|
|
27
27
|
|
|
@@ -41,10 +41,9 @@ diagnosticsFullWaitTimeoutMs / diagnosticsRequestTimeoutMs / initializeTimeoutMs
|
|
|
41
41
|
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
42
42
|
| `include` | 文件 glob(相对项目根或调用 cwd,任一命中即可);缺省匹配所有文件 |
|
|
43
43
|
| `kind` | `language`(缺省)或 `linter`。rename / inspect 等符号级功能**只面向 `language`**;linter 只参与诊断 |
|
|
44
|
-
| `
|
|
44
|
+
| `workingDir` | 服务器工作目录(即 LSP root):绝对路径或相对调用 cwd 的路径;缺省即调用 cwd。文件必须位于该目录内才会由本服务器处理;spawn 工作目录与 rootUri 均用它 |
|
|
45
45
|
| `bin` | 可执行文件:绝对路径、相对调用 cwd 的路径、或仅名字(项目工作区优先,PATH 兜底) |
|
|
46
46
|
| `args` | 启动参数数组 |
|
|
47
|
-
| `cwd` | 启动工作目录,支持 `{root}` / `{cwd}` 模板;缺省 `{root}` |
|
|
48
47
|
| `env` | 追加的环境变量:string 值支持 `{root}`/`{cwd}` 与 `${VAR}` 插值;`{ "sh": ["cmd","arg"] }` 启动时执行命令取 stdout(非零退出/空输出 → 启动失败并报错) |
|
|
49
48
|
| `languageIdByExtension` | 扩展名(含点)→ languageId;缺省回退内置映射(见 `src/lib/lsp/language.ts`,覆盖主流语言) |
|
|
50
49
|
| `startupTimeoutMs` / `diagnosticsWaitMs` | 覆盖该服务器的初始化握手 / 写文件后诊断等待(缺省用全局配置与 client 默认) |
|
|
@@ -70,10 +69,8 @@ typescript-language-server **不内置 TypeScript**(零依赖)。启动时
|
|
|
70
69
|
"servers": {
|
|
71
70
|
"typescript": {
|
|
72
71
|
"include": ["**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}"],
|
|
73
|
-
"rootMarkers": ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"],
|
|
74
72
|
"bin": "typescript-language-server",
|
|
75
73
|
"args": ["--stdio"],
|
|
76
|
-
"cwd": "{root}",
|
|
77
74
|
"languageIdByExtension": {
|
|
78
75
|
".ts": "typescript",
|
|
79
76
|
".tsx": "typescriptreact",
|
|
@@ -94,11 +91,11 @@ typescript-language-server **不内置 TypeScript**(零依赖)。启动时
|
|
|
94
91
|
|
|
95
92
|
- 启动失败(binary 缺失 / initialize 拒绝)会**主动 notify 报错**(server id、root、原因、提示 `/lsp-reload <id>`),不需要等请求方触发
|
|
96
93
|
- 失败进入 60s 冷却:冷却内该服务器被跳过,**冷却过后下次触碰自动重试**——修复配置后不用重启 agent,等冷却过即可,或立即 `/lsp-reload <id>` 重启指定服务器(同时清除失败记录)
|
|
97
|
-
- 命令:`/lsp-reload <id>`(重启单个)、`/lsp-stop`(停全部并禁用)、`/lsp-start`(重新启用)
|
|
94
|
+
- 命令:`/lsp-reload <id>`(重启单个)、`/lsp-reload`(无参:重读配置并重启全部)、`/lsp-stop`(停全部并禁用)、`/lsp-start`(重新启用)
|
|
98
95
|
|
|
99
96
|
## 生效时机
|
|
100
97
|
|
|
101
|
-
- `
|
|
98
|
+
- 配置在 `session_start` 预读并**按 cwd 缓存**:cwd 变化或 `/lsp-reload`(单个或无参)时重读。改配置后用 `/lsp-reload` 让全部或单个服务器换新配置(已启动的进程随之重启)
|
|
102
99
|
- `session_start` 时若 enabled 服务器数为 0 则不创建 service,之后从无到有地启用需要重启 agent
|
|
103
100
|
|
|
104
101
|
## 常见排查
|
|
@@ -108,4 +105,4 @@ typescript-language-server **不内置 TypeScript**(零依赖)。启动时
|
|
|
108
105
|
| notify `failed to start …: binary not found` | `bin` 不在 PATH(测试场景二进制缺失时整组跳过) |
|
|
109
106
|
| `… provides no tsserver.js. No other valid TypeScript installation was found` | workspace 的 `node_modules/typescript` 是 alias stub:配 `initializationOptions.tsserver.path` 指 `.pnpm` 实体,或换标准 typescript 依赖 |
|
|
110
107
|
| 诊断一直为空且无任何报错 | 服务器没匹配到文件(`include`/扩展名)、在 broken 冷却中、或文件在调用 cwd 之外(LSP 只在工作目录内启用) |
|
|
111
|
-
| 读取配置直接抛错 | typebox
|
|
108
|
+
| 读取配置直接抛错 | typebox 严格校验拒绝:字段类型不符 / 非法时长格式(未知字段只是 warning,不拒绝) |
|