@trim21/personal-pi-extensions 0.0.315 → 0.0.317
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/claude-code/files.ts +15 -89
- package/src/lib/lsp/lsp.ts +57 -16
- package/src/opencode/files.ts +3 -4
package/package.json
CHANGED
package/src/claude-code/files.ts
CHANGED
|
@@ -14,8 +14,7 @@ import {
|
|
|
14
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { Type } from "typebox";
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
import { type ToolPendant } from "../lib/pendant.js";
|
|
17
|
+
import { type LspService, registerLsp } from "../lib/lsp/lsp.js";
|
|
19
18
|
import { guardWriteAccess } from "../lib/write-guard.js";
|
|
20
19
|
import {
|
|
21
20
|
type ClaudeCodeState,
|
|
@@ -46,43 +45,6 @@ function formatFileSize(bytes: number): string {
|
|
|
46
45
|
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
/** Edit/Write 结果的可折叠面板:diff(```diff 代码块)+ LSP errors(有则显示)。 */
|
|
50
|
-
export function buildEditPendant(options: {
|
|
51
|
-
filePath: string;
|
|
52
|
-
diff: string;
|
|
53
|
-
diagnosticText: string;
|
|
54
|
-
}): ToolPendant {
|
|
55
|
-
const lines = [`## Diff — \`${options.filePath}\``, "", "```diff", options.diff, "```"];
|
|
56
|
-
if (options.diagnosticText !== "") {
|
|
57
|
-
lines.push("", "## LSP errors", "", "```text", options.diagnosticText, "```");
|
|
58
|
-
}
|
|
59
|
-
return { markdown: lines.join("\n"), expanded: true };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Read 结果的可折叠面板:路径、总行数、读取范围与文件大小。 */
|
|
63
|
-
export function buildReadPendant(options: {
|
|
64
|
-
filePath: string;
|
|
65
|
-
byteSize: number;
|
|
66
|
-
offset?: number;
|
|
67
|
-
limit?: number;
|
|
68
|
-
totalLines?: number;
|
|
69
|
-
type?: string;
|
|
70
|
-
}): ToolPendant {
|
|
71
|
-
const lines = ["## Read", "", `- **Path**: \`${options.filePath}\``];
|
|
72
|
-
if (options.type) lines.push(`- **Type**: ${options.type}`);
|
|
73
|
-
if (options.totalLines !== undefined) {
|
|
74
|
-
const start = options.offset ?? 1;
|
|
75
|
-
const end = Math.min(
|
|
76
|
-
options.limit === undefined ? options.totalLines : start + options.limit - 1,
|
|
77
|
-
options.totalLines,
|
|
78
|
-
);
|
|
79
|
-
lines.push(`- **Total lines**: ${options.totalLines}`);
|
|
80
|
-
if (start <= end) lines.push(`- **Range**: ${start}-${end}`);
|
|
81
|
-
}
|
|
82
|
-
lines.push(`- **Size**: ${formatFileSize(options.byteSize)}`);
|
|
83
|
-
return { markdown: lines.join("\n"), expanded: false };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
48
|
/** Tool guidance, kept in markdown so it reads like documentation. */
|
|
87
49
|
const READ_PROMPT = readFileSync(fileURLToPath(new URL("read.md", import.meta.url)), "utf8").trim();
|
|
88
50
|
const EDIT_PROMPT = readFileSync(fileURLToPath(new URL("edit.md", import.meta.url)), "utf8").trim();
|
|
@@ -107,8 +69,6 @@ export interface FileToolDetails {
|
|
|
107
69
|
* session 文件,resume 后由 session_start 重建 reads state。
|
|
108
70
|
*/
|
|
109
71
|
reads?: Record<string, FileSnapshot>;
|
|
110
|
-
/** 可折叠的 diff / LSP 结果面板(pi TUI 渲染)。 */
|
|
111
|
-
pendant?: ToolPendant;
|
|
112
72
|
}
|
|
113
73
|
|
|
114
74
|
function snapshotOf(content: Uint8Array | string, textEditable = true): FileSnapshot {
|
|
@@ -326,17 +286,7 @@ export function registerFileTools(
|
|
|
326
286
|
const snapshot = snapshotOf(image, false);
|
|
327
287
|
const key = await readStateKey(filePath);
|
|
328
288
|
state.reads.set(key, snapshot);
|
|
329
|
-
return {
|
|
330
|
-
content,
|
|
331
|
-
details: {
|
|
332
|
-
reads: { [key]: snapshot },
|
|
333
|
-
pendant: buildReadPendant({
|
|
334
|
-
filePath,
|
|
335
|
-
byteSize: image.length,
|
|
336
|
-
type: imageMime,
|
|
337
|
-
}),
|
|
338
|
-
},
|
|
339
|
-
};
|
|
289
|
+
return { content, details: { reads: { [key]: snapshot } } };
|
|
340
290
|
}
|
|
341
291
|
|
|
342
292
|
const buffer = await readFile(filePath);
|
|
@@ -366,16 +316,7 @@ export function registerFileTools(
|
|
|
366
316
|
});
|
|
367
317
|
return {
|
|
368
318
|
content: [{ type: "text", text: formatted.text }],
|
|
369
|
-
details: {
|
|
370
|
-
reads: { [key]: snapshot },
|
|
371
|
-
pendant: buildReadPendant({
|
|
372
|
-
filePath,
|
|
373
|
-
byteSize: buffer.length,
|
|
374
|
-
offset: params.offset ?? 1,
|
|
375
|
-
limit: params.limit,
|
|
376
|
-
totalLines: formatted.totalLines,
|
|
377
|
-
}),
|
|
378
|
-
},
|
|
319
|
+
details: { reads: { [key]: snapshot } },
|
|
379
320
|
};
|
|
380
321
|
},
|
|
381
322
|
});
|
|
@@ -449,7 +390,9 @@ export function registerFileTools(
|
|
|
449
390
|
state.reads.set(key, snapshot);
|
|
450
391
|
const diff = generateDiffString("", newString);
|
|
451
392
|
throwIfAborted(signal);
|
|
452
|
-
const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd
|
|
393
|
+
const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
394
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
395
|
+
});
|
|
453
396
|
return [
|
|
454
397
|
`The file ${filePath} has been updated successfully.`,
|
|
455
398
|
{
|
|
@@ -528,7 +471,9 @@ export function registerFileTools(
|
|
|
528
471
|
? `The file ${filePath} has been updated. All occurrences were successfully replaced.`
|
|
529
472
|
: `The file ${filePath} has been updated successfully.`;
|
|
530
473
|
throwIfAborted(signal);
|
|
531
|
-
const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd
|
|
474
|
+
const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
475
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
476
|
+
});
|
|
532
477
|
return [
|
|
533
478
|
text,
|
|
534
479
|
{
|
|
@@ -544,17 +489,7 @@ export function registerFileTools(
|
|
|
544
489
|
const text = diagnosticText
|
|
545
490
|
? `${message}\n\nLSP errors detected in this file, please fix:\n${diagnosticText}`
|
|
546
491
|
: message;
|
|
547
|
-
return {
|
|
548
|
-
content: [{ type: "text" as const, text }],
|
|
549
|
-
details: {
|
|
550
|
-
...details,
|
|
551
|
-
pendant: buildEditPendant({
|
|
552
|
-
filePath,
|
|
553
|
-
diff: details.diff ?? "",
|
|
554
|
-
diagnosticText,
|
|
555
|
-
}),
|
|
556
|
-
},
|
|
557
|
-
};
|
|
492
|
+
return { content: [{ type: "text" as const, text }], details };
|
|
558
493
|
},
|
|
559
494
|
});
|
|
560
495
|
|
|
@@ -616,7 +551,9 @@ export function registerFileTools(
|
|
|
616
551
|
? `File created successfully at: ${filePath}`
|
|
617
552
|
: `The file ${filePath} has been updated successfully.`;
|
|
618
553
|
throwIfAborted(signal);
|
|
619
|
-
const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd
|
|
554
|
+
const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
555
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
556
|
+
});
|
|
620
557
|
return [
|
|
621
558
|
text,
|
|
622
559
|
{
|
|
@@ -632,17 +569,7 @@ export function registerFileTools(
|
|
|
632
569
|
const text = diagnosticText
|
|
633
570
|
? `${message}\n\nLSP errors detected in this file, please fix:\n${diagnosticText}`
|
|
634
571
|
: message;
|
|
635
|
-
return {
|
|
636
|
-
content: [{ type: "text" as const, text }],
|
|
637
|
-
details: {
|
|
638
|
-
...details,
|
|
639
|
-
pendant: buildEditPendant({
|
|
640
|
-
filePath,
|
|
641
|
-
diff: details.diff ?? "",
|
|
642
|
-
diagnosticText,
|
|
643
|
-
}),
|
|
644
|
-
},
|
|
645
|
-
};
|
|
572
|
+
return { content: [{ type: "text" as const, text }], details };
|
|
646
573
|
},
|
|
647
574
|
});
|
|
648
575
|
}
|
|
@@ -677,8 +604,7 @@ function restoreFileReads(
|
|
|
677
604
|
* 与主进程 index.ts 聚合加载时的行为一致。
|
|
678
605
|
*/
|
|
679
606
|
export default function claudeCodeFileTools(pi: ExtensionAPI): void {
|
|
680
|
-
const service =
|
|
681
|
-
initLsp(pi, service);
|
|
607
|
+
const service = registerLsp(pi);
|
|
682
608
|
const state = createClaudeCodeState();
|
|
683
609
|
|
|
684
610
|
// 扩展实例在进程启动 / /reload / /new / /resume / /fork 时重建,内存里的
|
package/src/lib/lsp/lsp.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LSP 管理器:所有语言服务器连接的注册表与统一入口。
|
|
3
3
|
*
|
|
4
|
-
* - state(client 缓存、broken 集合、spawning 去重)是
|
|
4
|
+
* - state(client 缓存、broken 集合、spawning 去重)是 service 工厂的
|
|
5
5
|
* 闭包变量,不做成模块级全局;
|
|
6
6
|
* - 配置来源:全局 `~/.pi/agent/lsp.json` + 本地 `<cwd>/.pi/lsp.json`
|
|
7
7
|
* (本地逐字段覆盖全局):`servers` 数组配置驱动地定义语言服务器
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
* 参数继续生效;配置在每个工具的调用 cwd 下惰性读取;
|
|
10
10
|
* - client 按 (root, serverID) 缓存,并发 spawn 去重,启动失败记入 broken
|
|
11
11
|
* 集合(服务实例生命周期内不再重试);
|
|
12
|
-
* - 工具只与 touchFile / diagnostics / lspDiagnosticsForFile
|
|
12
|
+
* - 工具只与 touchFile / diagnostics / lspDiagnosticsForFile 三个方法打交道;通知回调按请求传入。
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { readFile } from "node:fs/promises";
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { extname, join, normalize, sep } from "node:path";
|
|
18
18
|
|
|
19
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import type { ExtensionAPI, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { type Static, Type } from "typebox";
|
|
21
21
|
import { Value } from "typebox/value";
|
|
22
22
|
|
|
@@ -130,12 +130,22 @@ interface LspState {
|
|
|
130
130
|
clients: LspClient[];
|
|
131
131
|
broken: Set<string>;
|
|
132
132
|
spawning: Map<string, Promise<LspClient | undefined>>;
|
|
133
|
+
closing: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface LspRequestOptions {
|
|
137
|
+
notify?: ExtensionUIContext["notify"];
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
export interface LspService {
|
|
136
|
-
touchFile(
|
|
141
|
+
touchFile(
|
|
142
|
+
file: string,
|
|
143
|
+
cwd: string,
|
|
144
|
+
diagnostics?: "document" | "full",
|
|
145
|
+
options?: LspRequestOptions,
|
|
146
|
+
): Promise<void>;
|
|
137
147
|
diagnostics(): Promise<Record<string, Diagnostic[]>>;
|
|
138
|
-
lspDiagnosticsForFile(file: string, cwd: string): Promise<string>;
|
|
148
|
+
lspDiagnosticsForFile(file: string, cwd: string, options?: LspRequestOptions): Promise<string>;
|
|
139
149
|
shutdownAll(): Promise<void>;
|
|
140
150
|
}
|
|
141
151
|
|
|
@@ -160,9 +170,15 @@ export function createLspService(
|
|
|
160
170
|
clients: [],
|
|
161
171
|
broken: new Set(),
|
|
162
172
|
spawning: new Map(),
|
|
173
|
+
closing: false,
|
|
163
174
|
};
|
|
164
175
|
|
|
165
|
-
async function getClients(
|
|
176
|
+
async function getClients(
|
|
177
|
+
file: string,
|
|
178
|
+
cwd: string,
|
|
179
|
+
notify?: ExtensionUIContext["notify"],
|
|
180
|
+
): Promise<LspClient[]> {
|
|
181
|
+
if (state.closing) return [];
|
|
166
182
|
if (!containsPath(file, cwd)) return [];
|
|
167
183
|
const config = await loadLspConfig(cwd, globalConfigPath);
|
|
168
184
|
const timeout = timeoutOptions(config);
|
|
@@ -195,6 +211,10 @@ export function createLspService(
|
|
|
195
211
|
const handle = await adapter.spawn(root, cwd);
|
|
196
212
|
if (!handle) {
|
|
197
213
|
state.broken.add(key);
|
|
214
|
+
notify?.(
|
|
215
|
+
`LSP server "${adapter.id}" is not available for ${root} (binary not found)`,
|
|
216
|
+
"error",
|
|
217
|
+
);
|
|
198
218
|
return;
|
|
199
219
|
}
|
|
200
220
|
const client = await create({
|
|
@@ -207,6 +227,10 @@ export function createLspService(
|
|
|
207
227
|
diagnosticsDocumentWaitTimeoutMs:
|
|
208
228
|
adapter.diagnosticsWaitMs ?? timeout.diagnosticsDocumentWaitTimeoutMs,
|
|
209
229
|
});
|
|
230
|
+
if (state.closing) {
|
|
231
|
+
await client.shutdown();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
210
234
|
const duplicate = state.clients.find((c) => c.root === root && c.serverID === adapter.id);
|
|
211
235
|
if (duplicate) {
|
|
212
236
|
await client.shutdown();
|
|
@@ -214,8 +238,14 @@ export function createLspService(
|
|
|
214
238
|
}
|
|
215
239
|
state.clients.push(client);
|
|
216
240
|
return client;
|
|
217
|
-
} catch {
|
|
241
|
+
} catch (error) {
|
|
218
242
|
state.broken.add(key);
|
|
243
|
+
notify?.(
|
|
244
|
+
`LSP server "${adapter.id}" failed to start for ${root}: ${
|
|
245
|
+
error instanceof Error ? error.message : String(error)
|
|
246
|
+
}`,
|
|
247
|
+
"error",
|
|
248
|
+
);
|
|
219
249
|
return;
|
|
220
250
|
}
|
|
221
251
|
})();
|
|
@@ -239,8 +269,9 @@ export function createLspService(
|
|
|
239
269
|
file: string,
|
|
240
270
|
cwd: string,
|
|
241
271
|
diagnostics?: "document" | "full",
|
|
272
|
+
options?: LspRequestOptions,
|
|
242
273
|
): Promise<void> {
|
|
243
|
-
const clients = await getClients(file, cwd);
|
|
274
|
+
const clients = await getClients(file, cwd, options?.notify);
|
|
244
275
|
await Promise.all(
|
|
245
276
|
clients.map(async (client) => {
|
|
246
277
|
const after = Date.now();
|
|
@@ -268,8 +299,12 @@ export function createLspService(
|
|
|
268
299
|
* edit/write 用:等待文档诊断并返回该文件的 ERROR 报告(空串表示无错误)。
|
|
269
300
|
* 内部所有 LSP 失败都会被吞掉,不干扰写操作本身。
|
|
270
301
|
*/
|
|
271
|
-
async function lspDiagnosticsForFile(
|
|
272
|
-
|
|
302
|
+
async function lspDiagnosticsForFile(
|
|
303
|
+
file: string,
|
|
304
|
+
cwd: string,
|
|
305
|
+
options?: LspRequestOptions,
|
|
306
|
+
): Promise<string> {
|
|
307
|
+
await touchFile(file, cwd, "document", options);
|
|
273
308
|
const all = await diagnostics();
|
|
274
309
|
const normalized = normalize(file);
|
|
275
310
|
return report(normalized, all[normalized] ?? []);
|
|
@@ -277,6 +312,8 @@ export function createLspService(
|
|
|
277
312
|
|
|
278
313
|
/** 终止全部服务器进程(session_shutdown 时调用)。 */
|
|
279
314
|
async function shutdownAll(): Promise<void> {
|
|
315
|
+
if (state.closing) return;
|
|
316
|
+
state.closing = true;
|
|
280
317
|
await Promise.all(state.clients.map((client) => client.shutdown())).catch(() => {
|
|
281
318
|
// 个别进程退出失败不阻止清理流程
|
|
282
319
|
});
|
|
@@ -287,10 +324,14 @@ export function createLspService(
|
|
|
287
324
|
return { touchFile, diagnostics, lspDiagnosticsForFile, shutdownAll };
|
|
288
325
|
}
|
|
289
326
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
327
|
+
export interface LspServiceOptions {
|
|
328
|
+
adapters?: LspServerAdapter[];
|
|
329
|
+
globalConfigPath?: string;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** 创建 LSP service 并注册 pi 的进程级清理生命周期。 */
|
|
333
|
+
export function registerLsp(pi: ExtensionAPI, options?: LspServiceOptions): LspService {
|
|
334
|
+
const service = createLspService(options?.adapters, options?.globalConfigPath);
|
|
335
|
+
pi.on?.("session_shutdown", () => service.shutdownAll());
|
|
336
|
+
return service;
|
|
296
337
|
}
|
package/src/opencode/files.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Opencode File Tools —— read / edit / write 统一构建点。
|
|
3
3
|
*
|
|
4
4
|
* 三个工具在同一个 registerFileTools(pi, service) 里注册,共享同一个 LSP
|
|
5
|
-
* service 实例(
|
|
5
|
+
* service 实例(registerLsp 创建的闭包变量),与 claude-code/files.ts 共享
|
|
6
6
|
* read-snapshot state 的方式一致;不再用模块级全局缓存。
|
|
7
7
|
*
|
|
8
8
|
* 各工具行为对齐 opencode commit 999be62662(v1.2.25-1672-g999be62662,
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from "@earendil-works/pi-coding-agent";
|
|
31
31
|
import { Type } from "typebox";
|
|
32
32
|
|
|
33
|
-
import {
|
|
33
|
+
import { type LspService, registerLsp } from "../lib/lsp/lsp.js";
|
|
34
34
|
import { guardWriteAccess } from "../lib/write-guard.js";
|
|
35
35
|
import {
|
|
36
36
|
detectLineEnding,
|
|
@@ -695,7 +695,6 @@ export function registerFileTools(pi: ExtensionAPI, service: LspService): void {
|
|
|
695
695
|
|
|
696
696
|
/** 独立入口:创建 LSP service(闭包共享给三个工具)并注册。 */
|
|
697
697
|
export default function opencodeFileTools(pi: ExtensionAPI): void {
|
|
698
|
-
const service =
|
|
699
|
-
initLsp(pi, service);
|
|
698
|
+
const service = registerLsp(pi);
|
|
700
699
|
registerFileTools(pi, service);
|
|
701
700
|
}
|