@mrclrchtr/supi-lsp 1.16.0 → 2.0.0

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 (77) hide show
  1. package/README.md +44 -56
  2. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +6 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
  4. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +8 -0
  5. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +10 -4
  6. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config.ts +1 -0
  7. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +1 -1
  8. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +6 -0
  9. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
  10. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +9 -2
  11. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +109 -4
  12. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +5 -1
  13. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
  14. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
  15. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +3 -2
  16. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +7 -0
  17. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +56 -2
  18. package/node_modules/@mrclrchtr/supi-code-runtime/src/evidence-badge.ts +40 -0
  19. package/node_modules/@mrclrchtr/supi-code-runtime/src/index.ts +5 -0
  20. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +76 -3
  21. package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/runtime.ts +37 -2
  22. package/node_modules/@mrclrchtr/supi-core/package.json +6 -1
  23. package/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
  24. package/node_modules/@mrclrchtr/supi-core/src/api.ts +8 -0
  25. package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +10 -4
  26. package/node_modules/@mrclrchtr/supi-core/src/config.ts +1 -0
  27. package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +1 -1
  28. package/node_modules/@mrclrchtr/supi-core/src/index.ts +6 -0
  29. package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
  30. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +9 -2
  31. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +109 -4
  32. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +5 -1
  33. package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
  34. package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
  35. package/node_modules/vscode-languageserver-protocol/lib/common/protocol.diagnostic.d.ts +21 -2
  36. package/node_modules/vscode-languageserver-protocol/lib/common/protocol.diagnostic.js +1 -0
  37. package/node_modules/vscode-languageserver-protocol/lib/common/protocol.inlineValue.d.ts +2 -1
  38. package/node_modules/vscode-languageserver-protocol/package.json +1 -1
  39. package/package.json +4 -31
  40. package/src/api.ts +27 -0
  41. package/src/client/client-refresh.ts +5 -0
  42. package/src/client/client.ts +246 -1
  43. package/src/client/transport.ts +4 -0
  44. package/src/config/capabilities.ts +3 -0
  45. package/src/config/config.ts +4 -2
  46. package/src/config/lsp-settings.ts +102 -0
  47. package/src/config/server-actions.ts +59 -0
  48. package/src/config/server-config.ts +4 -0
  49. package/src/index.ts +11 -0
  50. package/src/manager/capability-index.ts +1 -1
  51. package/src/manager/manager-project-info.ts +2 -1
  52. package/src/manager/manager.ts +115 -26
  53. package/src/provider/lsp-semantic-provider.ts +177 -134
  54. package/src/provider/refactor-planning.ts +214 -0
  55. package/src/session/runtime-controller.ts +320 -0
  56. package/src/session/runtime-registration.ts +26 -5
  57. package/src/session/service-registry.ts +81 -4
  58. package/src/summary.ts +2 -0
  59. package/src/extension.ts +0 -1
  60. package/src/format.ts +0 -373
  61. package/src/handlers/diagnostic-injection.ts +0 -182
  62. package/src/handlers/session-lifecycle.ts +0 -149
  63. package/src/handlers/status-command.ts +0 -38
  64. package/src/handlers/workspace-recovery.ts +0 -92
  65. package/src/lsp.ts +0 -37
  66. package/src/session/lsp-state.ts +0 -93
  67. package/src/session/settings-registration.ts +0 -296
  68. package/src/session/tree-persist.ts +0 -75
  69. package/src/tool/guidance.ts +0 -69
  70. package/src/tool/names.ts +0 -27
  71. package/src/tool/overrides.ts +0 -204
  72. package/src/tool/register-tools.ts +0 -69
  73. package/src/tool/service-actions.ts +0 -399
  74. package/src/tool/tool-specs.ts +0 -248
  75. package/src/ui/renderer.ts +0 -120
  76. package/src/ui/ui.ts +0 -360
  77. package/src/workspace-change.ts +0 -40
@@ -5,6 +5,8 @@
5
5
  import { type ChildProcess, spawn } from "node:child_process";
6
6
  import { existsSync } from "node:fs";
7
7
  import * as path from "node:path";
8
+ import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
9
+ import type { ProgressToken } from "vscode-languageserver-protocol";
8
10
  import { CLIENT_CAPABILITIES } from "../config/capabilities.ts";
9
11
  import type {
10
12
  CodeAction,
@@ -68,6 +70,15 @@ export class LspClient {
68
70
  /** Listeners waiting for diagnostics on a specific uri */
69
71
  private diagnosticWaiters = new Map<string, Array<() => void>>();
70
72
 
73
+ // ── Readiness (work-done-progress) ──────────────────────────────────
74
+ private trackedTokens = new Map<ProgressToken, "begin-seen" | "ended">();
75
+ private _readyPromise: Promise<void> | null = null;
76
+ private _readyResolve: (() => void) | undefined;
77
+ private _readyReject: ((err: Error) => void) | undefined;
78
+ private _isReady = false;
79
+ private noProgressTimer: ReturnType<typeof setTimeout> | null = null;
80
+ private tokenTimeouts = new Map<ProgressToken, ReturnType<typeof setTimeout>>();
81
+
71
82
  constructor(
72
83
  name: string,
73
84
  private readonly config: ServerConfig,
@@ -89,6 +100,11 @@ export class LspClient {
89
100
  return this.capabilities;
90
101
  }
91
102
 
103
+ /** Whether the server is currently not indexing and ready to serve queries. */
104
+ get ready(): boolean {
105
+ return this._isReady;
106
+ }
107
+
92
108
  // ── Lifecycle ───────────────────────────────────────────────────────
93
109
  /** Spawn the server process and perform the initialize handshake. */
94
110
  async start(): Promise<void> {
@@ -118,6 +134,8 @@ export class LspClient {
118
134
  this.rpc.onNotification((method, params) => {
119
135
  if (method === "textDocument/publishDiagnostics") {
120
136
  this.handlePublishDiagnostics(params as PublishDiagnosticsParams);
137
+ } else if (method === "$/progress") {
138
+ this.handleProgress(params as { token: ProgressToken; value: { kind: string } });
121
139
  }
122
140
  });
123
141
  this.rpc.onRequest((method, params) => this.handleServerRequest(method, params));
@@ -126,6 +144,8 @@ export class LspClient {
126
144
  this.process.on("exit", (_code) => {
127
145
  if (this._status !== "shutdown") {
128
146
  this._status = "error";
147
+ this.cancelNoProgressTimer();
148
+ this.rejectReady(new Error("Client crashed"));
129
149
  }
130
150
  this.rpc?.dispose();
131
151
  });
@@ -133,6 +153,7 @@ export class LspClient {
133
153
  this.process.on("error", (_err) => {
134
154
  if (this._status !== "shutdown") {
135
155
  this._status = "error";
156
+ this.rejectReady(new Error("Client process error"));
136
157
  }
137
158
  });
138
159
 
@@ -151,6 +172,8 @@ export class LspClient {
151
172
  this.capabilities = result.capabilities;
152
173
  this.rpc.sendNotification("initialized", {});
153
174
  this._status = "running";
175
+
176
+ this.armNoProgressTimer();
154
177
  } catch (err) {
155
178
  this._status = "error";
156
179
  this.process.kill();
@@ -203,6 +226,15 @@ export class LspClient {
203
226
  this.openDocs.clear();
204
227
  this.diagnosticStore.clear();
205
228
  this.releaseAllDiagnosticWaiters();
229
+
230
+ // Clear readiness state
231
+ if (this.noProgressTimer) {
232
+ clearTimeout(this.noProgressTimer);
233
+ this.noProgressTimer = null;
234
+ }
235
+ for (const timer of this.tokenTimeouts.values()) clearTimeout(timer);
236
+ this.tokenTimeouts.clear();
237
+ this.rejectReady(new Error("Client shutdown"));
206
238
  }
207
239
 
208
240
  // ── Document Synchronization ────────────────────────────────────────
@@ -468,6 +500,7 @@ export class LspClient {
468
500
  private async request<T>(method: string, params: unknown): Promise<T | null> {
469
501
  if (!this.rpc || this._status !== "running") return null;
470
502
  try {
503
+ await this.getReady();
471
504
  return (await this.rpc.sendRequest(method, params)) as T;
472
505
  } catch {
473
506
  return null;
@@ -482,8 +515,24 @@ export class LspClient {
482
515
  return [{ uri: fileToUri(this.root), name: path.basename(this.root) || this.root }];
483
516
  case "client/registerCapability":
484
517
  case "client/unregisterCapability":
485
- case "window/workDoneProgress/create":
486
518
  return null;
519
+ case "window/workDoneProgress/create": {
520
+ const token = (params as { token: ProgressToken }).token;
521
+ this.trackedTokens.set(token, "begin-seen");
522
+ this.cancelNoProgressTimer();
523
+ this._isReady = false;
524
+ if (!this._readyPromise) {
525
+ this._readyPromise = new Promise<void>((resolve, reject) => {
526
+ this._readyResolve = resolve;
527
+ this._readyReject = reject;
528
+ });
529
+ // Prevent unhandled rejection when rejectReady fires before any
530
+ // consumer is actively awaiting this promise (e.g. during shutdown).
531
+ this._readyPromise.catch(() => {});
532
+ }
533
+ this.startTokenTimeout(token);
534
+ return null;
535
+ }
487
536
  default:
488
537
  throw new JsonRpcRequestError(-32601, `Method not found: ${method}`);
489
538
  }
@@ -569,4 +618,200 @@ export class LspClient {
569
618
  this.diagnosticWaiters.delete(uri);
570
619
  for (const waiter of waiters) waiter();
571
620
  }
621
+
622
+ // ── Readiness (work-done-progress) ──────────────────────────────────
623
+
624
+ /**
625
+ * Wait for the server to be ready to serve queries.
626
+ * Returns immediately if already ready; returns the ongoing promise
627
+ * if one is pending; creates and returns a new one otherwise.
628
+ */
629
+ async getReady(): Promise<void> {
630
+ if (this._isReady) return;
631
+ if (this._readyPromise !== null) return this._readyPromise;
632
+ // If no progress timer was ever armed and no tokens are tracked,
633
+ // the server was either never started with a real process (test scenario)
634
+ // or completed before any progress tracking began. Resolve immediately
635
+ // only when the client is still running — a crash or shutdown clears
636
+ // both fields but must not report the client as ready.
637
+ if (this.noProgressTimer === null && this.trackedTokens.size === 0) {
638
+ if (this._status === "running") this._isReady = true;
639
+ return;
640
+ }
641
+ this._readyPromise = new Promise<void>((resolve, reject) => {
642
+ this._readyResolve = resolve;
643
+ this._readyReject = reject;
644
+ });
645
+ // Prevent unhandled rejection when rejectReady fires before any
646
+ // consumer is actively awaiting this promise (e.g. during shutdown).
647
+ this._readyPromise.catch(() => {});
648
+ return this._readyPromise;
649
+ }
650
+
651
+ /** Handle the $/progress notification from the server. */
652
+ private handleProgress(params: { token: ProgressToken; value: { kind: string } }): void {
653
+ const { token, value } = params;
654
+ // Defensive: guard against malformed notifications without a value.
655
+ if (!value || typeof value.kind !== "string") return;
656
+
657
+ if (value.kind === "begin") {
658
+ recordDebugEvent({
659
+ source: "lsp",
660
+ level: "debug",
661
+ category: "readiness.progress-begin",
662
+ message: `Readiness progress begin for token ${token}`,
663
+ data: { token },
664
+ });
665
+ // Cancel the 2s no-progress grace timer — a server that sends
666
+ // begin without a prior create is spec-deviant but valid.
667
+ this.cancelNoProgressTimer();
668
+ this.trackedTokens.set(token, "begin-seen");
669
+ this._isReady = false;
670
+ // Re-arm readiness promise if not already pending
671
+ if (!this._readyPromise) {
672
+ this._readyPromise = new Promise<void>((resolve, reject) => {
673
+ this._readyResolve = resolve;
674
+ this._readyReject = reject;
675
+ });
676
+ // Prevent unhandled rejection when rejectReady fires before any
677
+ // consumer is actively awaiting this promise (e.g. during shutdown).
678
+ this._readyPromise.catch(() => {});
679
+ }
680
+ this.startTokenTimeout(token);
681
+ } else if (value.kind === "end") {
682
+ recordDebugEvent({
683
+ source: "lsp",
684
+ level: "debug",
685
+ category: "readiness.progress-end",
686
+ message: `Readiness progress end for token ${token}`,
687
+ data: { token },
688
+ });
689
+ this.trackedTokens.set(token, "ended");
690
+ this.clearTokenTimeout(token);
691
+ this.checkAllTokensEnded();
692
+ }
693
+ // kind: "report" — intentionally no-op
694
+ }
695
+
696
+ /** Check whether all tracked tokens have ended and resolve readiness. */
697
+ private checkAllTokensEnded(): void {
698
+ if (this.trackedTokens.size === 0) return;
699
+ for (const state of this.trackedTokens.values()) {
700
+ if (state !== "ended") return;
701
+ }
702
+ this.trackedTokens.clear();
703
+ this.resolveReady();
704
+ }
705
+
706
+ /** Resolve the current readiness promise (if any) and mark the client ready. */
707
+ private resolveReady(): void {
708
+ if (this._readyResolve) {
709
+ this._readyResolve();
710
+ this._readyPromise = null;
711
+ this._readyResolve = undefined;
712
+ this._readyReject = undefined;
713
+ }
714
+ this._isReady = true;
715
+ recordDebugEvent({
716
+ source: "lsp",
717
+ level: "info",
718
+ category: "readiness.resolved",
719
+ message: `LSP client ${this.name} is ready (cwd: ${this.root})`,
720
+ });
721
+ }
722
+
723
+ /**
724
+ * Reject the current readiness promise (if any) and mark the client
725
+ * not ready. Called on shutdown, crash, or restart.
726
+ */
727
+ private rejectReady(reason: Error): void {
728
+ if (this._readyReject) {
729
+ this._readyReject(reason);
730
+ this._readyPromise = null;
731
+ this._readyResolve = undefined;
732
+ this._readyReject = undefined;
733
+ }
734
+ this._isReady = false;
735
+ this.trackedTokens.clear();
736
+ for (const timer of this.tokenTimeouts.values()) clearTimeout(timer);
737
+ this.tokenTimeouts.clear();
738
+ recordDebugEvent({
739
+ source: "lsp",
740
+ level: this._status === "shutdown" ? "debug" : "warning",
741
+ category: "readiness.rejected",
742
+ message: `LSP client ${this.name} readiness rejected: ${reason.message}`,
743
+ data: { status: this._status },
744
+ });
745
+ }
746
+
747
+ /**
748
+ * Start a per-token timeout. If the token never receives an "end",
749
+ * force-end it after `readinessTimeoutMs` (default 10s).
750
+ */
751
+ private startTokenTimeout(token: ProgressToken): void {
752
+ // Clear any existing timeout for this token (e.g., if both
753
+ // window/workDoneProgress/create and $/progress begin fire).
754
+ this.clearTokenTimeout(token);
755
+ const timeoutMs = this.config.readinessTimeoutMs ?? 10_000;
756
+ const timer = setTimeout(() => {
757
+ this.trackedTokens.set(token, "ended");
758
+ this.tokenTimeouts.delete(token);
759
+ recordDebugEvent({
760
+ source: "lsp",
761
+ level: "debug",
762
+ category: "readiness.token-timeout",
763
+ message: `Readiness per-token timeout fired for token ${token} after ${timeoutMs}ms`,
764
+ data: { token, timeoutMs },
765
+ });
766
+ this.checkAllTokensEnded();
767
+ }, timeoutMs);
768
+ this.tokenTimeouts.set(token, timer);
769
+ }
770
+
771
+ /** Clear the per-token timeout for a completed token. */
772
+ private clearTokenTimeout(token: ProgressToken): void {
773
+ const timer = this.tokenTimeouts.get(token);
774
+ if (timer) {
775
+ clearTimeout(timer);
776
+ this.tokenTimeouts.delete(token);
777
+ }
778
+ }
779
+
780
+ /** Cancel the 2s no-progress grace timer. */
781
+ private cancelNoProgressTimer(): void {
782
+ if (this.noProgressTimer) {
783
+ clearTimeout(this.noProgressTimer);
784
+ this.noProgressTimer = null;
785
+ recordDebugEvent({
786
+ source: "lsp",
787
+ level: "debug",
788
+ category: "readiness.no-progress-cancelled",
789
+ message: `No-progress grace timer cancelled for ${this.name}`,
790
+ });
791
+ }
792
+ }
793
+
794
+ /**
795
+ * Arm the 2s no-progress grace timer. If no server-initiated
796
+ * progress token arrives within this window, the server is treated
797
+ * as immediately ready (small project or non-progress-supporting server).
798
+ *
799
+ * A server that sends its first $/progress begin after 2s causes
800
+ * a brief false-ready window — the steady-state re-entrancy in
801
+ * handleProgress() flips isReady back to false when the begin arrives.
802
+ */
803
+ private armNoProgressTimer(): void {
804
+ this.noProgressTimer = setTimeout(() => {
805
+ if (this.trackedTokens.size === 0 && this._status === "running") {
806
+ recordDebugEvent({
807
+ source: "lsp",
808
+ level: "debug",
809
+ category: "readiness.no-progress-resolved",
810
+ message: `No-progress grace timer resolved for ${this.name}`,
811
+ });
812
+ this.resolveReady();
813
+ }
814
+ this.noProgressTimer = null;
815
+ }, 2_000);
816
+ }
572
817
  }
@@ -93,6 +93,10 @@ export class JsonRpcClient {
93
93
  const timer = setTimeout(() => tokenSource.cancel(), timeoutMs);
94
94
 
95
95
  const request = this.connection.sendRequest(method, params, tokenSource.token);
96
+ // Catch the raw request promise to prevent unhandled rejections when
97
+ // dispose() cancels the token without a preceding timeout (the raced
98
+ // promise below covers the timeout-then-dispose path separately).
99
+ request.catch(() => {});
96
100
 
97
101
  // Race the request against a timeout so callers don't hang forever.
98
102
  // The CancellationToken is also passed to sendRequest so the connection
@@ -53,6 +53,9 @@ export const CLIENT_CAPABILITIES: ClientCapabilities = {
53
53
  relatedDocumentSupport: true,
54
54
  },
55
55
  },
56
+ window: {
57
+ workDoneProgress: true,
58
+ },
56
59
  workspace: {
57
60
  workspaceFolders: false,
58
61
  diagnostics: {
@@ -2,13 +2,15 @@
2
2
 
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { loadSupiConfigForScope } from "@mrclrchtr/supi-core/config";
6
7
  import type { LspConfig, ServerConfig } from "./types.ts";
7
8
 
9
+ const CONFIG_DIR = path.dirname(fileURLToPath(import.meta.url));
10
+
8
11
  // Load defaults at module level — resolve relative to this file.
9
- // pi loads extensions via jiti, which always provides __dirname.
10
12
  const DEFAULTS: LspConfig = JSON.parse(
11
- fs.readFileSync(path.join(__dirname, "defaults.json"), "utf-8"),
13
+ fs.readFileSync(path.join(CONFIG_DIR, "defaults.json"), "utf-8"),
12
14
  ) as LspConfig;
13
15
 
14
16
  // ── Public API ────────────────────────────────────────────────────────
@@ -0,0 +1,102 @@
1
+ // LSP settings helpers — config loading for LSP session lifecycle.
2
+ //
3
+ // These are the non-UI config helpers extracted from settings-registration.ts
4
+ // so they can be consumed by the library-only runtime controller without
5
+ // importing pi-specific UI modules.
6
+
7
+ import { loadSupiConfig, loadSupiConfigForScope } from "@mrclrchtr/supi-core/config";
8
+
9
+ // ── Types ────────────────────────────────────────────────────
10
+
11
+ export interface LspSettings {
12
+ enabled: boolean;
13
+ severity: number;
14
+ active: string[];
15
+ exclude: string[];
16
+ }
17
+
18
+ export const LSP_DEFAULTS: LspSettings = {
19
+ enabled: true,
20
+ severity: 1,
21
+ active: [],
22
+ exclude: [],
23
+ };
24
+
25
+ // ── Config helpers ───────────────────────────────────────────
26
+
27
+ /**
28
+ * Load LSP settings from supi config for the given cwd.
29
+ *
30
+ * Merges project and global scopes with defaults, returning the
31
+ * effective LspSettings for session startup.
32
+ */
33
+ export function loadLspSettings(cwd: string, homeDir?: string): LspSettings {
34
+ return loadSupiConfig("lsp", cwd, LSP_DEFAULTS, { homeDir });
35
+ }
36
+
37
+ /**
38
+ * Return a user-facing message explaining why LSP may appear disabled.
39
+ *
40
+ * **Since the always-on policy:** `lsp.enabled: false` is deprecated and
41
+ * ignored. This function is kept for backward compatibility with the
42
+ * settings UI and reports that LSP is always attempted.
43
+ */
44
+ export function getLspDisabledMessage(cwd: string, homeDir?: string): string {
45
+ const global = loadSupiConfigForScope("lsp", cwd, LSP_DEFAULTS, { scope: "global", homeDir });
46
+ const project = loadSupiConfigForScope("lsp", cwd, LSP_DEFAULTS, { scope: "project", homeDir });
47
+
48
+ if (project.enabled === false) {
49
+ return "LSP is always attempted; `lsp.enabled: false` in project settings (.pi/supi/config.json) is deprecated and ignored";
50
+ }
51
+ if (global.enabled === false) {
52
+ return "LSP is always attempted; `lsp.enabled: false` in global settings (~/.pi/agent/supi/config.json) is deprecated and ignored";
53
+ }
54
+ return "LSP is always attempted";
55
+ }
56
+
57
+ // ── Deprecated-key detection (always-on policy) ────────────────
58
+
59
+ /**
60
+ * Result of checking for deprecated LSP configuration keys.
61
+ */
62
+ export interface DeprecatedLspKeys {
63
+ /** `lsp.enabled` presence in project config. */
64
+ projectEnabled: boolean;
65
+ /** `lsp.enabled` presence in global config. */
66
+ globalEnabled: boolean;
67
+ /** `lsp.active` presence in project config. */
68
+ projectActive: boolean;
69
+ /** `lsp.active` presence in global config. */
70
+ globalActive: boolean;
71
+ }
72
+
73
+ /**
74
+ * Check whether deprecated `lsp.enabled` or `lsp.active` keys exist
75
+ * in project or global config. These keys are ignored by the always-on
76
+ * LSP policy but may be present in user config.
77
+ */
78
+ export function getDeprecatedLspKeys(cwd: string, homeDir?: string): DeprecatedLspKeys {
79
+ const global = loadSupiConfigForScope("lsp", cwd, LSP_DEFAULTS, { scope: "global", homeDir });
80
+ const project = loadSupiConfigForScope("lsp", cwd, LSP_DEFAULTS, { scope: "project", homeDir });
81
+
82
+ return {
83
+ projectEnabled: hasExplicitEnabledKey(project),
84
+ globalEnabled: hasExplicitEnabledKey(global),
85
+ projectActive: hasExplicitActiveKey(project),
86
+ globalActive: hasExplicitActiveKey(global),
87
+ };
88
+ }
89
+
90
+ /** Whether the loaded settings had an explicit `enabled` key (different from default). */
91
+ function hasExplicitEnabledKey(settings: Partial<{ enabled: boolean }>): boolean {
92
+ return settings.enabled === false;
93
+ }
94
+
95
+ /** Whether the loaded settings had an explicit `active` key. */
96
+ function hasExplicitActiveKey(settings: Partial<{ active: string[] }>): boolean {
97
+ return Array.isArray(settings.active) && settings.active.length > 0;
98
+ }
99
+
100
+ // Alias for backwards compatibility with the RED test name
101
+ /** @deprecated Use {@link getDeprecatedLspKeys} instead. */
102
+ export const hasDeprecatedLspKeys: typeof getDeprecatedLspKeys = getDeprecatedLspKeys;
@@ -0,0 +1,59 @@
1
+ // Server capability action label helpers — maps LSP capabilities to readable action labels.
2
+
3
+ import type { ServerCapabilities } from "../config/types.ts";
4
+
5
+ interface LspServerSupportedActionSpec {
6
+ label: string;
7
+ isSupported: (capabilities: ServerCapabilities | null | undefined) => boolean;
8
+ }
9
+
10
+ const LSP_SERVER_SUPPORTED_ACTION_SPECS: readonly LspServerSupportedActionSpec[] = [
11
+ {
12
+ label: "diagnostics [optional file]",
13
+ isSupported: () => true,
14
+ },
15
+ {
16
+ label: "hover(file,line,char)",
17
+ isSupported: (capabilities) => Boolean(capabilities?.hoverProvider),
18
+ },
19
+ {
20
+ label: "definition(file,line,char)",
21
+ isSupported: (capabilities) => Boolean(capabilities?.definitionProvider),
22
+ },
23
+ {
24
+ label: "references(file,line,char)",
25
+ isSupported: (capabilities) => Boolean(capabilities?.referencesProvider),
26
+ },
27
+ {
28
+ label: "implementation(file,line,char)",
29
+ isSupported: (capabilities) => Boolean(capabilities?.implementationProvider),
30
+ },
31
+ {
32
+ label: "symbols(file)",
33
+ isSupported: (capabilities) => Boolean(capabilities?.documentSymbolProvider),
34
+ },
35
+ {
36
+ label: "workspace_symbols(query)",
37
+ isSupported: (capabilities) => Boolean(capabilities?.workspaceSymbolProvider),
38
+ },
39
+ {
40
+ label: "rename(file,line,char,newName)",
41
+ isSupported: (capabilities) => Boolean(capabilities?.renameProvider),
42
+ },
43
+ {
44
+ label: "code_actions(file,line,char)",
45
+ isSupported: (capabilities) => Boolean(capabilities?.codeActionProvider),
46
+ },
47
+ ] as const;
48
+
49
+ /**
50
+ * Get the list of supported LSP server actions based on capabilities.
51
+ */
52
+ export function getSupportedLspServerActions(
53
+ capabilities: ServerCapabilities | null | undefined,
54
+ ): string[] {
55
+ if (!capabilities) return [];
56
+ return LSP_SERVER_SUPPORTED_ACTION_SPECS.filter((spec) => spec.isSupported(capabilities)).map(
57
+ (spec) => spec.label,
58
+ );
59
+ }
@@ -8,6 +8,8 @@ export interface ServerConfig {
8
8
  rootMarkers: string[];
9
9
  enabled?: boolean;
10
10
  initializationOptions?: unknown;
11
+ /** Maximum time to wait for a single $/progress cycle, in ms. Default: 10_000. */
12
+ readinessTimeoutMs?: number;
11
13
  }
12
14
 
13
15
  /** LSP configuration keyed by language name (e.g. `typescript`, `python`). */
@@ -25,6 +27,8 @@ export interface ProjectServerInfo extends DetectedProjectServer {
25
27
  status: "running" | "error" | "unavailable";
26
28
  supportedActions: string[];
27
29
  openFiles: string[];
30
+ /** Whether the LSP server is currently not indexing and ready to serve queries. */
31
+ ready: boolean;
28
32
  }
29
33
 
30
34
  /** A language whose source files are present but the server binary is missing. */
package/src/index.ts CHANGED
@@ -2,6 +2,11 @@
2
2
  // Consumers should import the published API surface from
3
3
  // `@mrclrchtr/supi-lsp/api`, not the package root.
4
4
 
5
+ export type { LspSettings } from "./config/lsp-settings.ts";
6
+ export {
7
+ getLspDisabledMessage,
8
+ loadLspSettings,
9
+ } from "./config/lsp-settings.ts";
5
10
  export type {
6
11
  CodeAction,
7
12
  Diagnostic,
@@ -17,9 +22,15 @@ export type {
17
22
  WorkspaceSymbol,
18
23
  } from "./config/types.ts";
19
24
  export { toLspPosition, toOneBasedPosition } from "./coordinates.ts";
25
+ export type {
26
+ LspControllerState,
27
+ LspStartResult,
28
+ } from "./session/runtime-controller.ts";
29
+ export { LspRuntimeController } from "./session/runtime-controller.ts";
20
30
  export type {
21
31
  OutstandingDiagnosticSummaryEntry,
22
32
  RecoverDiagnosticsResult,
33
+ SemanticReadinessResult,
23
34
  SessionLspServiceState,
24
35
  WorkspaceDiagnosticSummaryEntry,
25
36
  } from "./session/service-registry.ts";
@@ -1,7 +1,7 @@
1
1
  // Capability index — indexes and queries server capabilities.
2
2
 
3
+ import { getSupportedLspServerActions } from "../config/server-actions.ts";
3
4
  import type { ServerCapabilities } from "../config/types.ts";
4
- import { getSupportedLspServerActions } from "../tool/tool-specs.ts";
5
5
  import type { LspManager } from "./manager.ts";
6
6
 
7
7
  /**
@@ -1,7 +1,7 @@
1
1
  import type { LspClient } from "../client/client.ts";
2
+ import { getSupportedLspServerActions } from "../config/server-actions.ts";
2
3
  import type { ProjectServerInfo } from "../config/server-config.ts";
3
4
  import { displayRelativeFilePath } from "../summary.ts";
4
- import { getSupportedLspServerActions } from "../tool/tool-specs.ts";
5
5
 
6
6
  interface ProjectServerInfoInput {
7
7
  serverName: string;
@@ -31,5 +31,6 @@ export function buildProjectServerInfo(
31
31
  status,
32
32
  supportedActions: getSupportedLspServerActions(input.client?.serverCapabilities),
33
33
  openFiles: input.client?.openFiles.map((file) => displayRelativeFilePath(file, cwd)) ?? [],
34
+ ready: input.client?.ready ?? false,
34
35
  };
35
36
  }