@trim21/personal-pi-extensions 0.1.513 → 0.1.514

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/lib/lsp/lsp.ts +160 -104
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.513",
3
+ "version": "0.1.514",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -425,14 +425,17 @@ export interface LspService {
425
425
  start(): void;
426
426
  /**
427
427
  * 重启指定服务器:关闭其全部 client、清除对应失败记录并解除禁用;
428
- * 其余服务器不受影响。配置缓存失效,下次工具调用按当前 cwd 重新读盘。
428
+ * 其余服务器不受影响。配置缓存失效并立即重读盘上配置:此前运行中的该
429
+ * 服务器若仍存在于新配置则马上重启,未在运行的服务器保持惰性。
430
+ * 返回成功重启的 server id。
429
431
  */
430
- reload(serverID: string): Promise<void>;
432
+ reload(serverID: string): Promise<string[]>;
431
433
  /**
432
434
  * 重启全部服务器(/lsp-reload 无参):关闭所有 client、清空失败记录并
433
- * 解除禁用;配置缓存失效,下次工具调用重新读盘。
435
+ * 解除禁用;配置缓存失效并立即重读盘上配置,此前运行中的服务器若仍存在
436
+ * 于新配置则马上重启。返回成功重启的 server id。
434
437
  */
435
- reloadAll(): Promise<void>;
438
+ reloadAll(): Promise<string[]>;
436
439
  /** 已知服务器 id(running 或 broken 的去重集合),供命令补全与提示。 */
437
440
  serverIDs(): string[];
438
441
  /** 注入 status 渲染回调;传入 undefined 表示不再渲染。 */
@@ -632,49 +635,115 @@ export function createLspService(
632
635
  updateStatusText();
633
636
  }
634
637
 
638
+ /**
639
+ * 记录一次启动失败:进入 broken(冷却期内跳过)、渲染 status,并按节流
640
+ * 间隔主动 notify。错误上报优先走会话级 sessionNotify——不依赖触发请求
641
+ * 恰好携带 notify(否则 Read warm-up 等静默通道会把失败吞掉);未注入
642
+ * 会话通知时退回请求级 notify 兜底。
643
+ */
644
+ function reportStartupFailure(
645
+ key: string,
646
+ serverID: string,
647
+ root: string,
648
+ cause: string,
649
+ notify?: ExtensionUIContext["notify"],
650
+ ): void {
651
+ const now = Date.now();
652
+ state.brokenFailAt.set(key, now);
653
+ state.servers.set(key, { serverID, root, state: "broken" });
654
+ updateStatusText();
655
+ const reporter = sessionNotify ?? notify;
656
+ const lastNotified = state.brokenNotifiedAt.get(key);
657
+ if (reporter && (lastNotified === undefined || now - lastNotified >= notifyIntervalMs)) {
658
+ state.brokenNotifiedAt.set(key, now);
659
+ reporter(
660
+ `LSP server "${serverID}" failed to start for ${root}: ${cause}. ` +
661
+ `Fix the issue or run /lsp-reload ${serverID} to retry now.`,
662
+ "error",
663
+ );
664
+ }
665
+ }
666
+
667
+ /**
668
+ * spawn 指定 adapter 并注册 client:同一 key 的 in-flight spawn 复用其结果,
669
+ * 失败进 broken(节流上报,返回 undefined),成功后注册 client / extensions /
670
+ * status 并确保 watcher 运行。供 getClients 与 reload 后的立即重启共用。
671
+ */
672
+ async function startClient(
673
+ adapter: LspServerAdapter,
674
+ root: string,
675
+ cwd: string,
676
+ config: ResolvedLspConfig,
677
+ notify?: ExtensionUIContext["notify"],
678
+ ): Promise<LspClient | undefined> {
679
+ const key = root + adapter.id;
680
+ const inflight = state.spawning.get(key);
681
+ if (inflight) return inflight;
682
+ const task = (async () => {
683
+ try {
684
+ const handle = await adapter.spawn(root, cwd);
685
+ if (!handle) {
686
+ reportStartupFailure(key, adapter.id, root, "binary not found", notify);
687
+ return;
688
+ }
689
+ const client = await create({
690
+ serverID: adapter.id,
691
+ server: handle,
692
+ root,
693
+ directory: cwd,
694
+ diagnosticsDebounceMs: config.diagnosticsDebounceMs,
695
+ diagnosticsDocumentWaitTimeoutMs:
696
+ adapter.diagnosticsWaitMs ?? config.diagnosticsDocumentWaitTimeoutMs,
697
+ diagnosticsFullWaitTimeoutMs: config.diagnosticsFullWaitTimeoutMs,
698
+ diagnosticsRequestTimeoutMs: config.diagnosticsRequestTimeoutMs,
699
+ initializeTimeoutMs: adapter.startupTimeoutMs ?? config.initializeTimeoutMs,
700
+ maxOpenDocuments: config.maxOpenDocuments,
701
+ });
702
+ if (state.closing || state.disabled) {
703
+ await client.shutdown();
704
+ return;
705
+ }
706
+ const duplicate = state.clients.find((c) => c.root === root && c.serverID === adapter.id);
707
+ if (duplicate) {
708
+ await client.shutdown();
709
+ return duplicate;
710
+ }
711
+ state.clients.push(client);
712
+ state.clientExtensions.set(client, adapter.extensions);
713
+ state.servers.set(key, { serverID: adapter.id, root, state: "running" });
714
+ // 启动成功:清除失败记录,之后若再次失败会立即重新上报
715
+ state.brokenFailAt.delete(key);
716
+ state.brokenNotifiedAt.delete(key);
717
+ updateStatusText();
718
+ void ensureWatcher(cwd, notify);
719
+ return client;
720
+ } catch (error) {
721
+ reportStartupFailure(
722
+ key,
723
+ adapter.id,
724
+ root,
725
+ error instanceof Error ? error.message : String(error),
726
+ notify,
727
+ );
728
+ return;
729
+ }
730
+ })();
731
+ state.spawning.set(key, task);
732
+ void task.finally(() => {
733
+ if (state.spawning.get(key) === task) state.spawning.delete(key);
734
+ });
735
+ return task;
736
+ }
737
+
635
738
  async function getClients(
636
739
  file: string,
637
740
  cwd: string,
638
741
  notify?: ExtensionUIContext["notify"],
639
742
  adapterFilter?: (adapter: LspServerAdapter) => boolean,
640
743
  ): Promise<LspClient[]> {
641
- /**
642
- * 记录一次启动失败:进入 broken(冷却期内跳过)、渲染 status,并按节流
643
- * 间隔主动 notify。错误上报优先走会话级 sessionNotify——不依赖触发请求
644
- * 恰好携带 notify(否则 Read warm-up 等静默通道会把失败吞掉);未注入
645
- * 会话通知时退回请求级 notify 兜底。
646
- */
647
- function reportStartupFailure(
648
- key: string,
649
- serverID: string,
650
- root: string,
651
- cause: string,
652
- ): void {
653
- const now = Date.now();
654
- state.brokenFailAt.set(key, now);
655
- state.servers.set(key, { serverID, root, state: "broken" });
656
- updateStatusText();
657
- const reporter = sessionNotify ?? notify;
658
- const lastNotified = state.brokenNotifiedAt.get(key);
659
- if (reporter && (lastNotified === undefined || now - lastNotified >= notifyIntervalMs)) {
660
- state.brokenNotifiedAt.set(key, now);
661
- reporter(
662
- `LSP server "${serverID}" failed to start for ${root}: ${cause}. ` +
663
- `Fix the issue or run /lsp-reload ${serverID} to retry now.`,
664
- "error",
665
- );
666
- }
667
- }
668
744
  if (state.closing || state.disabled) return [];
669
745
  if (!containsPath(file, cwd)) return [];
670
746
  const config = await currentConfig(cwd);
671
- const timeout = {
672
- diagnosticsDebounceMs: config.diagnosticsDebounceMs,
673
- diagnosticsDocumentWaitTimeoutMs: config.diagnosticsDocumentWaitTimeoutMs,
674
- diagnosticsFullWaitTimeoutMs: config.diagnosticsFullWaitTimeoutMs,
675
- diagnosticsRequestTimeoutMs: config.diagnosticsRequestTimeoutMs,
676
- initializeTimeoutMs: config.initializeTimeoutMs,
677
- };
678
747
  const active = filterAdapters(adapters ?? createAdapters(config.servers), config);
679
748
  const extension = extname(file) || file;
680
749
  const result: LspClient[] = [];
@@ -705,65 +774,7 @@ export function createLspService(
705
774
  continue;
706
775
  }
707
776
 
708
- const inflight = state.spawning.get(key);
709
- if (inflight) {
710
- const client = await inflight;
711
- if (client) result.push(client);
712
- continue;
713
- }
714
-
715
- const task = (async () => {
716
- try {
717
- const handle = await adapter.spawn(root, cwd);
718
- if (!handle) {
719
- reportStartupFailure(key, adapter.id, root, "binary not found");
720
- return;
721
- }
722
- const client = await create({
723
- serverID: adapter.id,
724
- server: handle,
725
- root,
726
- directory: cwd,
727
- ...timeout,
728
- initializeTimeoutMs: adapter.startupTimeoutMs ?? timeout.initializeTimeoutMs,
729
- diagnosticsDocumentWaitTimeoutMs:
730
- adapter.diagnosticsWaitMs ?? timeout.diagnosticsDocumentWaitTimeoutMs,
731
- maxOpenDocuments: config.maxOpenDocuments,
732
- });
733
- if (state.closing || state.disabled) {
734
- await client.shutdown();
735
- return;
736
- }
737
- const duplicate = state.clients.find((c) => c.root === root && c.serverID === adapter.id);
738
- if (duplicate) {
739
- await client.shutdown();
740
- return duplicate;
741
- }
742
- state.clients.push(client);
743
- state.clientExtensions.set(client, adapter.extensions);
744
- state.servers.set(key, { serverID: adapter.id, root, state: "running" });
745
- // 启动成功:清除失败记录,之后若再次失败会立即重新上报
746
- state.brokenFailAt.delete(key);
747
- state.brokenNotifiedAt.delete(key);
748
- updateStatusText();
749
- void ensureWatcher(cwd, notify);
750
- return client;
751
- } catch (error) {
752
- reportStartupFailure(
753
- key,
754
- adapter.id,
755
- root,
756
- error instanceof Error ? error.message : String(error),
757
- );
758
- return;
759
- }
760
- })();
761
- state.spawning.set(key, task);
762
- void task.finally(() => {
763
- if (state.spawning.get(key) === task) state.spawning.delete(key);
764
- });
765
-
766
- const client = await task;
777
+ const client = await startClient(adapter, root, cwd, config, notify);
767
778
  if (client) result.push(client);
768
779
  }
769
780
 
@@ -979,9 +990,43 @@ export function createLspService(
979
990
  updateStatusText();
980
991
  }
981
992
 
982
- async function reload(serverID: string): Promise<void> {
993
+ /**
994
+ * reload 后立即重启此前运行中的服务器,不再等下一次工具调用。只重启新配置
995
+ * 中仍存在且启用的 server;无运行记录(如 /lsp-stop 之后)或配置重读失败时
996
+ * 不动,保持惰性 spawn。返回成功重启的 server id。
997
+ */
998
+ async function respawnRunning(serverIDs: readonly string[]): Promise<string[]> {
999
+ const cwd = state.cwd;
1000
+ if (!cwd) return [];
1001
+ let config: ResolvedLspConfig;
1002
+ try {
1003
+ config = await currentConfig(cwd);
1004
+ } catch (error) {
1005
+ sessionNotify?.(
1006
+ `LSP reload: re-reading config failed, servers will start on the next tool call: ${
1007
+ error instanceof Error ? error.message : String(error)
1008
+ }`,
1009
+ "warning",
1010
+ );
1011
+ return [];
1012
+ }
1013
+ const active = filterAdapters(adapters ?? createAdapters(config.servers), config);
1014
+ const restarted = await Promise.all(
1015
+ serverIDs.map(async (serverID): Promise<string | undefined> => {
1016
+ const adapter = active.find((candidate) => candidate.id === serverID);
1017
+ if (!adapter) return;
1018
+ const root = serverRoot(adapter.workingDir, cwd);
1019
+ const client = await startClient(adapter, root, cwd, config);
1020
+ return client ? serverID : undefined;
1021
+ }),
1022
+ );
1023
+ return restarted.filter((id): id is string => id !== undefined);
1024
+ }
1025
+
1026
+ async function reload(serverID: string): Promise<string[]> {
1027
+ const wasRunning = state.clients.some((client) => client.serverID === serverID);
983
1028
  state.closing = true;
984
- // 配置缓存失效:下次工具调用按当前 cwd 重新读盘,让配置修改生效
1029
+ // 配置缓存失效:立即重读盘上配置,让配置修改生效
985
1030
  state.config = undefined;
986
1031
  state.configCwd = undefined;
987
1032
  const targets = state.clients.filter((client) => client.serverID === serverID);
@@ -1000,9 +1045,12 @@ export function createLspService(
1000
1045
  state.closing = false;
1001
1046
  state.disabled = false;
1002
1047
  updateStatusText();
1048
+ if (!wasRunning) return [];
1049
+ return respawnRunning([serverID]);
1003
1050
  }
1004
1051
 
1005
- async function reloadAll(): Promise<void> {
1052
+ async function reloadAll(): Promise<string[]> {
1053
+ const runningIDs = [...new Set(state.clients.map((client) => client.serverID))];
1006
1054
  state.closing = true;
1007
1055
  state.config = undefined;
1008
1056
  state.configCwd = undefined;
@@ -1018,6 +1066,7 @@ export function createLspService(
1018
1066
  state.closing = false;
1019
1067
  state.disabled = false;
1020
1068
  updateStatusText();
1069
+ return respawnRunning(runningIDs);
1021
1070
  }
1022
1071
 
1023
1072
  function serverIDs(): string[] {
@@ -1211,13 +1260,20 @@ export function createLspManager(
1211
1260
  const serverID = args.trim();
1212
1261
  if (!serverID) {
1213
1262
  // 无参:重读配置并重启全部服务器
1214
- await service.reloadAll();
1215
- ctx.ui.notify("LSP reloaded: all servers will restart on the next tool call", "info");
1263
+ const restarted = await service.reloadAll();
1264
+ ctx.ui.notify(
1265
+ restarted.length > 0
1266
+ ? `LSP reloaded: ${restarted.toSorted().join(", ")} restarted`
1267
+ : "LSP reloaded: servers will restart on the next tool call",
1268
+ "info",
1269
+ );
1216
1270
  return;
1217
1271
  }
1218
- await service.reload(serverID);
1272
+ const restarted = await service.reload(serverID);
1219
1273
  ctx.ui.notify(
1220
- `LSP server "${serverID}" reloaded: will restart on the next tool call`,
1274
+ restarted.length > 0
1275
+ ? `LSP server "${serverID}" reloaded`
1276
+ : `LSP server "${serverID}" reloaded: will restart on the next tool call`,
1221
1277
  "info",
1222
1278
  );
1223
1279
  },