@trim21/personal-pi-extensions 0.1.512 → 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.
- package/package.json +1 -1
- package/src/aft/tools.ts +99 -6
- package/src/lib/lsp/lsp.ts +160 -104
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,14 +425,17 @@ export interface LspService {
|
|
|
425
425
|
start(): void;
|
|
426
426
|
/**
|
|
427
427
|
* 重启指定服务器:关闭其全部 client、清除对应失败记录并解除禁用;
|
|
428
|
-
*
|
|
428
|
+
* 其余服务器不受影响。配置缓存失效并立即重读盘上配置:此前运行中的该
|
|
429
|
+
* 服务器若仍存在于新配置则马上重启,未在运行的服务器保持惰性。
|
|
430
|
+
* 返回成功重启的 server id。
|
|
429
431
|
*/
|
|
430
|
-
reload(serverID: string): Promise<
|
|
432
|
+
reload(serverID: string): Promise<string[]>;
|
|
431
433
|
/**
|
|
432
434
|
* 重启全部服务器(/lsp-reload 无参):关闭所有 client、清空失败记录并
|
|
433
|
-
*
|
|
435
|
+
* 解除禁用;配置缓存失效并立即重读盘上配置,此前运行中的服务器若仍存在
|
|
436
|
+
* 于新配置则马上重启。返回成功重启的 server id。
|
|
434
437
|
*/
|
|
435
|
-
reloadAll(): Promise<
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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<
|
|
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(
|
|
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
|
-
|
|
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
|
},
|