@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.316",
3
+ "version": "0.0.317",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -14,7 +14,7 @@ import {
14
14
  } from "@earendil-works/pi-coding-agent";
15
15
  import { Type } from "typebox";
16
16
 
17
- import { createLspService, initLsp, type LspService } from "../lib/lsp/lsp.js";
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 = createLspService();
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
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * LSP 管理器:所有语言服务器连接的注册表与统一入口。
3
3
  *
4
- * - state(client 缓存、broken 集合、spawning 去重)是 createLspService
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(file: string, cwd: string, diagnostics?: "document" | "full"): Promise<void>;
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(file: string, cwd: string): Promise<LspClient[]> {
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
- ui?.notify(
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
- ui?.notify(
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(file: string, cwd: string): Promise<string> {
285
- await touchFile(file, cwd, "document");
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
- /** 注册进程级生命周期:session_shutdown 时清理全部服务器进程。 */
312
- export function initLsp(pi: ExtensionAPI, service: LspService): void {
313
- // 测试里的 fake pi 没有事件订阅;生产环境 pi.on 必然存在
314
- pi.on?.("session_shutdown", () => {
315
- void service.shutdownAll();
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
  }
@@ -2,7 +2,7 @@
2
2
  * Opencode File Tools —— read / edit / write 统一构建点。
3
3
  *
4
4
  * 三个工具在同一个 registerFileTools(pi, service) 里注册,共享同一个 LSP
5
- * service 实例(createLspService 的闭包变量),与 claude-code/files.ts 共享
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 { createLspService, initLsp, type LspService } from "../lib/lsp/lsp.js";
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 = createLspService();
699
- initLsp(pi, service);
698
+ const service = registerLsp(pi);
700
699
  registerFileTools(pi, service);
701
700
  }