@trim21/personal-pi-extensions 0.0.316 → 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 +11 -8
- package/src/lib/lsp/lsp.ts +48 -28
- package/src/opencode/files.ts +3 -4
package/package.json
CHANGED
package/src/claude-code/files.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { Type } from "typebox";
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { type LspService, registerLsp } from "../lib/lsp/lsp.js";
|
|
18
18
|
import { guardWriteAccess } from "../lib/write-guard.js";
|
|
19
19
|
import {
|
|
20
20
|
type ClaudeCodeState,
|
|
@@ -390,7 +390,9 @@ export function registerFileTools(
|
|
|
390
390
|
state.reads.set(key, snapshot);
|
|
391
391
|
const diff = generateDiffString("", newString);
|
|
392
392
|
throwIfAborted(signal);
|
|
393
|
-
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
|
+
});
|
|
394
396
|
return [
|
|
395
397
|
`The file ${filePath} has been updated successfully.`,
|
|
396
398
|
{
|
|
@@ -469,7 +471,9 @@ export function registerFileTools(
|
|
|
469
471
|
? `The file ${filePath} has been updated. All occurrences were successfully replaced.`
|
|
470
472
|
: `The file ${filePath} has been updated successfully.`;
|
|
471
473
|
throwIfAborted(signal);
|
|
472
|
-
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
|
+
});
|
|
473
477
|
return [
|
|
474
478
|
text,
|
|
475
479
|
{
|
|
@@ -547,7 +551,9 @@ export function registerFileTools(
|
|
|
547
551
|
? `File created successfully at: ${filePath}`
|
|
548
552
|
: `The file ${filePath} has been updated successfully.`;
|
|
549
553
|
throwIfAborted(signal);
|
|
550
|
-
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
|
+
});
|
|
551
557
|
return [
|
|
552
558
|
text,
|
|
553
559
|
{
|
|
@@ -598,8 +604,7 @@ function restoreFileReads(
|
|
|
598
604
|
* 与主进程 index.ts 聚合加载时的行为一致。
|
|
599
605
|
*/
|
|
600
606
|
export default function claudeCodeFileTools(pi: ExtensionAPI): void {
|
|
601
|
-
const service =
|
|
602
|
-
initLsp(pi, service);
|
|
607
|
+
const service = registerLsp(pi);
|
|
603
608
|
const state = createClaudeCodeState();
|
|
604
609
|
|
|
605
610
|
// 扩展实例在进程启动 / /reload / /new / /resume / /fork 时重建,内存里的
|
|
@@ -607,7 +612,6 @@ export default function claudeCodeFileTools(pi: ExtensionAPI): void {
|
|
|
607
612
|
// 若文件在此期间被外部修改,Edit/Write 时的指纹对比仍会要求重新 Read,
|
|
608
613
|
// 防呆语义不因重建而弱化。
|
|
609
614
|
pi.on("session_start", (_event, ctx) => {
|
|
610
|
-
service.setUi(ctx.ui);
|
|
611
615
|
restoreFileReads(state, ctx.sessionManager);
|
|
612
616
|
});
|
|
613
617
|
|
|
@@ -615,7 +619,6 @@ export default function claudeCodeFileTools(pi: ExtensionAPI): void {
|
|
|
615
619
|
// session_start,扩展实例也不重建。这里同样重放当前分支,丢弃被抛弃分支
|
|
616
620
|
// 的记账,避免 state 与当前分支脱节。
|
|
617
621
|
pi.on("session_tree", (_event, ctx) => {
|
|
618
|
-
service.setUi(ctx.ui);
|
|
619
622
|
restoreFileReads(state, ctx.sessionManager);
|
|
620
623
|
});
|
|
621
624
|
|
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,7 +9,7 @@
|
|
|
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";
|
|
@@ -130,15 +130,23 @@ 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
|
-
/** 绑定当前会话的 UI 上下文,LSP 服务器启动失败时用它发错误通知(传 undefined 解绑)。 */
|
|
141
|
-
setUi(ui?: Pick<ExtensionUIContext, "notify">): void;
|
|
142
150
|
}
|
|
143
151
|
|
|
144
152
|
/** 文件必须在工作目录内才启用 LSP(对齐 opencode 的 containsPath)。 */
|
|
@@ -162,10 +170,15 @@ export function createLspService(
|
|
|
162
170
|
clients: [],
|
|
163
171
|
broken: new Set(),
|
|
164
172
|
spawning: new Map(),
|
|
173
|
+
closing: false,
|
|
165
174
|
};
|
|
166
|
-
let ui: Pick<ExtensionUIContext, "notify"> | undefined;
|
|
167
175
|
|
|
168
|
-
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 [];
|
|
169
182
|
if (!containsPath(file, cwd)) return [];
|
|
170
183
|
const config = await loadLspConfig(cwd, globalConfigPath);
|
|
171
184
|
const timeout = timeoutOptions(config);
|
|
@@ -198,7 +211,7 @@ export function createLspService(
|
|
|
198
211
|
const handle = await adapter.spawn(root, cwd);
|
|
199
212
|
if (!handle) {
|
|
200
213
|
state.broken.add(key);
|
|
201
|
-
|
|
214
|
+
notify?.(
|
|
202
215
|
`LSP server "${adapter.id}" is not available for ${root} (binary not found)`,
|
|
203
216
|
"error",
|
|
204
217
|
);
|
|
@@ -214,6 +227,10 @@ export function createLspService(
|
|
|
214
227
|
diagnosticsDocumentWaitTimeoutMs:
|
|
215
228
|
adapter.diagnosticsWaitMs ?? timeout.diagnosticsDocumentWaitTimeoutMs,
|
|
216
229
|
});
|
|
230
|
+
if (state.closing) {
|
|
231
|
+
await client.shutdown();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
217
234
|
const duplicate = state.clients.find((c) => c.root === root && c.serverID === adapter.id);
|
|
218
235
|
if (duplicate) {
|
|
219
236
|
await client.shutdown();
|
|
@@ -223,7 +240,7 @@ export function createLspService(
|
|
|
223
240
|
return client;
|
|
224
241
|
} catch (error) {
|
|
225
242
|
state.broken.add(key);
|
|
226
|
-
|
|
243
|
+
notify?.(
|
|
227
244
|
`LSP server "${adapter.id}" failed to start for ${root}: ${
|
|
228
245
|
error instanceof Error ? error.message : String(error)
|
|
229
246
|
}`,
|
|
@@ -252,8 +269,9 @@ export function createLspService(
|
|
|
252
269
|
file: string,
|
|
253
270
|
cwd: string,
|
|
254
271
|
diagnostics?: "document" | "full",
|
|
272
|
+
options?: LspRequestOptions,
|
|
255
273
|
): Promise<void> {
|
|
256
|
-
const clients = await getClients(file, cwd);
|
|
274
|
+
const clients = await getClients(file, cwd, options?.notify);
|
|
257
275
|
await Promise.all(
|
|
258
276
|
clients.map(async (client) => {
|
|
259
277
|
const after = Date.now();
|
|
@@ -281,8 +299,12 @@ export function createLspService(
|
|
|
281
299
|
* edit/write 用:等待文档诊断并返回该文件的 ERROR 报告(空串表示无错误)。
|
|
282
300
|
* 内部所有 LSP 失败都会被吞掉,不干扰写操作本身。
|
|
283
301
|
*/
|
|
284
|
-
async function lspDiagnosticsForFile(
|
|
285
|
-
|
|
302
|
+
async function lspDiagnosticsForFile(
|
|
303
|
+
file: string,
|
|
304
|
+
cwd: string,
|
|
305
|
+
options?: LspRequestOptions,
|
|
306
|
+
): Promise<string> {
|
|
307
|
+
await touchFile(file, cwd, "document", options);
|
|
286
308
|
const all = await diagnostics();
|
|
287
309
|
const normalized = normalize(file);
|
|
288
310
|
return report(normalized, all[normalized] ?? []);
|
|
@@ -290,6 +312,8 @@ export function createLspService(
|
|
|
290
312
|
|
|
291
313
|
/** 终止全部服务器进程(session_shutdown 时调用)。 */
|
|
292
314
|
async function shutdownAll(): Promise<void> {
|
|
315
|
+
if (state.closing) return;
|
|
316
|
+
state.closing = true;
|
|
293
317
|
await Promise.all(state.clients.map((client) => client.shutdown())).catch(() => {
|
|
294
318
|
// 个别进程退出失败不阻止清理流程
|
|
295
319
|
});
|
|
@@ -297,21 +321,17 @@ export function createLspService(
|
|
|
297
321
|
state.broken.clear();
|
|
298
322
|
}
|
|
299
323
|
|
|
300
|
-
return {
|
|
301
|
-
touchFile,
|
|
302
|
-
diagnostics,
|
|
303
|
-
lspDiagnosticsForFile,
|
|
304
|
-
shutdownAll,
|
|
305
|
-
setUi(nextUi) {
|
|
306
|
-
ui = nextUi;
|
|
307
|
-
},
|
|
308
|
-
};
|
|
324
|
+
return { touchFile, diagnostics, lspDiagnosticsForFile, shutdownAll };
|
|
309
325
|
}
|
|
310
326
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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;
|
|
317
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
|
}
|