@mrclrchtr/supi-lsp 1.16.1 → 2.0.1

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 (73) 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/package.json +4 -31
  36. package/src/api.ts +27 -0
  37. package/src/client/client-refresh.ts +5 -0
  38. package/src/client/client.ts +246 -1
  39. package/src/client/transport.ts +4 -0
  40. package/src/config/capabilities.ts +3 -0
  41. package/src/config/config.ts +4 -2
  42. package/src/config/lsp-settings.ts +102 -0
  43. package/src/config/server-actions.ts +59 -0
  44. package/src/config/server-config.ts +4 -0
  45. package/src/index.ts +11 -0
  46. package/src/manager/capability-index.ts +1 -1
  47. package/src/manager/manager-project-info.ts +2 -1
  48. package/src/manager/manager.ts +115 -26
  49. package/src/provider/lsp-semantic-provider.ts +177 -134
  50. package/src/provider/refactor-planning.ts +214 -0
  51. package/src/session/runtime-controller.ts +320 -0
  52. package/src/session/runtime-registration.ts +26 -5
  53. package/src/session/service-registry.ts +81 -4
  54. package/src/summary.ts +2 -0
  55. package/src/extension.ts +0 -1
  56. package/src/format.ts +0 -373
  57. package/src/handlers/diagnostic-injection.ts +0 -182
  58. package/src/handlers/session-lifecycle.ts +0 -149
  59. package/src/handlers/status-command.ts +0 -38
  60. package/src/handlers/workspace-recovery.ts +0 -92
  61. package/src/lsp.ts +0 -37
  62. package/src/session/lsp-state.ts +0 -93
  63. package/src/session/settings-registration.ts +0 -296
  64. package/src/session/tree-persist.ts +0 -75
  65. package/src/tool/guidance.ts +0 -69
  66. package/src/tool/names.ts +0 -27
  67. package/src/tool/overrides.ts +0 -204
  68. package/src/tool/register-tools.ts +0 -69
  69. package/src/tool/service-actions.ts +0 -399
  70. package/src/tool/tool-specs.ts +0 -248
  71. package/src/ui/renderer.ts +0 -120
  72. package/src/ui/ui.ts +0 -360
  73. package/src/workspace-change.ts +0 -40
@@ -0,0 +1,214 @@
1
+ import type { CodePosition, RefactorResult, SourceRange } from "@mrclrchtr/supi-code-runtime/api";
2
+ import type { CodeAction, TextDocumentEdit, TextEdit, WorkspaceEdit } from "../config/types.ts";
3
+ import type { SessionLspService } from "../session/service-registry.ts";
4
+
5
+ export async function runRenameRefactor(
6
+ lsp: SessionLspService,
7
+ file: string,
8
+ position: CodePosition,
9
+ newName: string,
10
+ ): Promise<RefactorResult> {
11
+ const edit = await lsp.rename(file, position, newName);
12
+ return convertLspWorkspaceEdit(edit);
13
+ }
14
+
15
+ export function collectCodeActionResults(actions: CodeAction[]): RefactorResult[] {
16
+ const results: RefactorResult[] = [];
17
+ for (const action of actions) {
18
+ if (!action.edit) {
19
+ results.push({
20
+ kind: "unavailable",
21
+ reason: `Code action "${action.title}" has no edit`,
22
+ });
23
+ continue;
24
+ }
25
+
26
+ const result = convertCodeActionToResult(action);
27
+ results.push(
28
+ result?.kind === "precise"
29
+ ? result
30
+ : {
31
+ kind: "unavailable",
32
+ reason: `Code action "${action.title}" could not produce precise edits`,
33
+ },
34
+ );
35
+ }
36
+ return results;
37
+ }
38
+
39
+ export async function runFilteredCodeActionRefactor(options: {
40
+ lsp: SessionLspService;
41
+ file: string;
42
+ position: CodePosition;
43
+ operation: "update_imports" | "delete_dead_code" | "extract_function" | "extract_variable";
44
+ range?: SourceRange;
45
+ matches: (action: CodeAction) => boolean;
46
+ }): Promise<RefactorResult> {
47
+ const { lsp, file, position, operation, matches } = options;
48
+ const actions = await lsp.codeActions(file, options.range ?? position);
49
+ if (!actions || actions.length === 0) {
50
+ return {
51
+ kind: "unavailable",
52
+ reason: `No code actions are available for refactor operation "${operation}".`,
53
+ };
54
+ }
55
+
56
+ const matching = actions.filter(matches);
57
+ if (matching.length === 0) {
58
+ return {
59
+ kind: "unavailable",
60
+ reason: `No matching precise code action is available for refactor operation "${operation}".`,
61
+ };
62
+ }
63
+
64
+ for (const action of matching) {
65
+ const converted = convertCodeActionToResult(action);
66
+ if (converted?.kind === "precise") {
67
+ return converted;
68
+ }
69
+ }
70
+
71
+ return {
72
+ kind: "unavailable",
73
+ reason: `Matching code actions for refactor operation "${operation}" did not produce precise edits.`,
74
+ };
75
+ }
76
+
77
+ export function isUpdateImportsCodeAction(action: CodeAction): boolean {
78
+ const kind = action.kind ?? "";
79
+ const title = action.title.trim().toLowerCase();
80
+ const kindlessTitleMatch = kind === "" && title === "organize imports";
81
+ return (
82
+ kind === "source.organizeImports" ||
83
+ kind.startsWith("source.organizeImports.") ||
84
+ kindlessTitleMatch
85
+ );
86
+ }
87
+
88
+ export function isExtractFunctionCodeAction(action: CodeAction): boolean {
89
+ const kind = action.kind ?? "";
90
+ const title = action.title.trim().toLowerCase();
91
+ return (
92
+ kind === "refactor.extract.function" ||
93
+ kind.startsWith("refactor.extract.function.") ||
94
+ (title.includes("extract") && /\b(function|method)\b/.test(title))
95
+ );
96
+ }
97
+
98
+ export function isExtractVariableCodeAction(action: CodeAction): boolean {
99
+ const kind = action.kind ?? "";
100
+ const title = action.title.trim().toLowerCase();
101
+ return (
102
+ kind === "refactor.extract.constant" ||
103
+ kind.startsWith("refactor.extract.constant.") ||
104
+ kind === "refactor.extract.variable" ||
105
+ kind.startsWith("refactor.extract.variable.") ||
106
+ (title.includes("extract") && /\b(constant|const|variable)\b/.test(title))
107
+ );
108
+ }
109
+
110
+ export function isDeleteDeadCodeCodeAction(action: CodeAction): boolean {
111
+ const kind = action.kind ?? "";
112
+ const title = action.title.trim().toLowerCase();
113
+ const kindMatches =
114
+ kind === "quickfix" ||
115
+ kind.startsWith("quickfix.") ||
116
+ kind === "refactor.rewrite" ||
117
+ kind.startsWith("refactor.rewrite.");
118
+ const titleMatches =
119
+ /(unused|dead code|remove unused|remove unreachable|remove declaration)/.test(title);
120
+ return kindMatches && titleMatches;
121
+ }
122
+
123
+ function convertCodeActionToResult(action: CodeAction): RefactorResult | null {
124
+ if (!action.edit) {
125
+ return null;
126
+ }
127
+ return convertLspWorkspaceEdit(action.edit);
128
+ }
129
+
130
+ function convertLspWorkspaceEdit(edit: WorkspaceEdit | null): RefactorResult {
131
+ if (!edit) {
132
+ return { kind: "unavailable", reason: "LSP server returned no edit" };
133
+ }
134
+
135
+ let fileEdits = edit.documentChanges?.length
136
+ ? collectDocumentChangeEdits(edit.documentChanges)
137
+ : [];
138
+ if (fileEdits.length === 0 && edit.changes) {
139
+ fileEdits = collectChangesEdits(edit.changes);
140
+ }
141
+
142
+ if (fileEdits.length === 0) {
143
+ return { kind: "unavailable", reason: "Workspace edit contains no file edits" };
144
+ }
145
+
146
+ return { kind: "precise", edits: { edits: fileEdits } };
147
+ }
148
+
149
+ function resolveFileFromUri(uri: string): string {
150
+ if (!uri.startsWith("file://")) return uri;
151
+ try {
152
+ return decodeURIComponent(uri.slice(7));
153
+ } catch {
154
+ return uri;
155
+ }
156
+ }
157
+
158
+ function collectDocumentChangeEdits(
159
+ docChanges: NonNullable<WorkspaceEdit["documentChanges"]>,
160
+ ): Array<{
161
+ file: string;
162
+ range: { start: { line: number; character: number }; end: { line: number; character: number } };
163
+ newText: string;
164
+ }> {
165
+ const out: Array<{
166
+ file: string;
167
+ range: { start: { line: number; character: number }; end: { line: number; character: number } };
168
+ newText: string;
169
+ }> = [];
170
+ for (const change of docChanges) {
171
+ const tdEdit = change as TextDocumentEdit;
172
+ if (!tdEdit.textDocument || !tdEdit.edits) continue;
173
+ const file = resolveFileFromUri(tdEdit.textDocument.uri);
174
+ for (const singleEdit of tdEdit.edits) {
175
+ const te = singleEdit as TextEdit;
176
+ out.push({
177
+ file,
178
+ range: {
179
+ start: { line: te.range.start.line, character: te.range.start.character },
180
+ end: { line: te.range.end.line, character: te.range.end.character },
181
+ },
182
+ newText: te.newText,
183
+ });
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+
189
+ function collectChangesEdits(changes: NonNullable<WorkspaceEdit["changes"]>): Array<{
190
+ file: string;
191
+ range: { start: { line: number; character: number }; end: { line: number; character: number } };
192
+ newText: string;
193
+ }> {
194
+ const out: Array<{
195
+ file: string;
196
+ range: { start: { line: number; character: number }; end: { line: number; character: number } };
197
+ newText: string;
198
+ }> = [];
199
+ for (const [uri, textEdits] of Object.entries(changes)) {
200
+ if (!textEdits || textEdits.length === 0) continue;
201
+ const file = resolveFileFromUri(uri);
202
+ for (const te of textEdits) {
203
+ out.push({
204
+ file,
205
+ range: {
206
+ start: { line: te.range.start.line, character: te.range.start.character },
207
+ end: { line: te.range.end.line, character: te.range.end.character },
208
+ },
209
+ newText: te.newText,
210
+ });
211
+ }
212
+ }
213
+ return out;
214
+ }
@@ -0,0 +1,320 @@
1
+ // LSP session runtime controller — pi-independent lifecycle for LSP session management.
2
+ //
3
+ // This controller owns session start/shutdown for one cwd:
4
+ // - Creates and disposes the LspManager
5
+ // - Publishes SessionLspService states through the existing registry
6
+ // - Exposes the data the umbrella adapter will need later
7
+ //
8
+ // It does NOT import pi event types or ExtensionAPI.
9
+
10
+ import type { WorkspaceRuntime } from "@mrclrchtr/supi-code-runtime/api";
11
+ import { loadConfig } from "../config/config.ts";
12
+ import { type LspSettings, loadLspSettings } from "../config/lsp-settings.ts";
13
+ import { clearTsconfigCache } from "../config/tsconfig-scope.ts";
14
+ import type { DetectedProjectServer, LspConfig, ProjectServerInfo } from "../config/types.ts";
15
+ import { scanWorkspaceSentinels } from "../diagnostics/workspace-sentinels.ts";
16
+ import { LspManager } from "../manager/manager.ts";
17
+ import {
18
+ markLspCapabilitiesReady,
19
+ registerPendingLspCapabilities,
20
+ unregisterLspCapabilities,
21
+ } from "./runtime-registration.ts";
22
+ import { scanMissingServers, scanProjectCapabilities, startDetectedServers } from "./scanner.ts";
23
+ import {
24
+ clearSessionLspService,
25
+ SessionLspService,
26
+ setSessionLspServiceState,
27
+ } from "./service-registry.ts";
28
+
29
+ // ── Types ─────────────────────────────────────────────────────────────
30
+
31
+ /** Starting state before {@link LspRuntimeController.start} is called. */
32
+ export type LspControllerState =
33
+ | LspControllerInitial
34
+ | LspControllerPending
35
+ | LspControllerReady
36
+ | LspControllerDisabled
37
+ | LspControllerUnavailable;
38
+
39
+ interface LspControllerInitial {
40
+ kind: "initial";
41
+ }
42
+
43
+ interface LspControllerPending {
44
+ kind: "pending";
45
+ }
46
+
47
+ interface LspControllerReady {
48
+ kind: "ready";
49
+ manager: LspManager;
50
+ service: SessionLspService;
51
+ projectServers: ProjectServerInfo[];
52
+ detectedServers: DetectedProjectServer[];
53
+ settings: LspSettings;
54
+ }
55
+
56
+ interface LspControllerDisabled {
57
+ kind: "disabled";
58
+ message: string;
59
+ }
60
+
61
+ interface LspControllerUnavailable {
62
+ kind: "unavailable";
63
+ reason: string;
64
+ }
65
+
66
+ /** Result type from {@link LspRuntimeController.start}. */
67
+ export type LspStartResult =
68
+ | { kind: "ready"; manager: LspManager; service: SessionLspService }
69
+ | { kind: "disabled"; message: string }
70
+ | { kind: "unavailable"; reason: string };
71
+
72
+ // ── Controller ────────────────────────────────────────────────────────
73
+
74
+ /**
75
+ * Pi-independent LSP session lifecycle controller.
76
+ *
77
+ * Use this in the umbrella extension (supi-code-intelligence) instead of
78
+ * reaching into substrate extension internals.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const controller = new LspRuntimeController(cwd);
83
+ * const result = await controller.start();
84
+ * if (result.kind === "ready") {
85
+ * // use controller.manager, controller.service
86
+ * }
87
+ * // later
88
+ * await controller.shutdown();
89
+ * ```
90
+ */
91
+ export class LspRuntimeController {
92
+ readonly #cwd: string;
93
+ #state: LspControllerState;
94
+ #runtime: WorkspaceRuntime | null;
95
+ #readinessGeneration = 0;
96
+
97
+ constructor(cwd: string, runtime?: WorkspaceRuntime) {
98
+ this.#cwd = cwd;
99
+ this.#state = { kind: "initial" };
100
+ this.#runtime = runtime ?? null;
101
+ }
102
+
103
+ /** The workspace cwd this controller was created for. */
104
+ get cwd(): string {
105
+ return this.#cwd;
106
+ }
107
+
108
+ /** Current controller state. */
109
+ get kind(): LspControllerState["kind"] {
110
+ return this.#state.kind;
111
+ }
112
+
113
+ /** The LspManager, only available when state is "ready". */
114
+ get manager(): LspManager | null {
115
+ return this.#state.kind === "ready" ? this.#state.manager : null;
116
+ }
117
+
118
+ /** The SessionLspService, only available when state is "ready". */
119
+ get service(): SessionLspService | null {
120
+ return this.#state.kind === "ready" ? this.#state.service : null;
121
+ }
122
+
123
+ /** Project server info, only available when state is "ready". */
124
+ get projectServers(): ProjectServerInfo[] {
125
+ if (this.#state.kind === "ready") return this.#state.projectServers;
126
+ return [];
127
+ }
128
+
129
+ /** Detected servers, only available when state is "ready". */
130
+ get detectedServers(): DetectedProjectServer[] {
131
+ if (this.#state.kind === "ready") return this.#state.detectedServers;
132
+ return [];
133
+ }
134
+
135
+ /** LSP settings used for this session. */
136
+ get settings(): LspSettings | null {
137
+ if (this.#state.kind === "ready") return this.#state.settings;
138
+ return null;
139
+ }
140
+
141
+ /** The WorkspaceRuntime registered for this session's cwd. */
142
+ get runtime(): WorkspaceRuntime | null {
143
+ return this.#runtime;
144
+ }
145
+
146
+ /** Attach a WorkspaceRuntime for capability registration. */
147
+ setRuntime(runtime: WorkspaceRuntime): void {
148
+ this.#runtime = runtime;
149
+ }
150
+
151
+ /**
152
+ * Start the LSP session for this controller's cwd.
153
+ *
154
+ * Loads settings, creates the manager, starts detected servers,
155
+ * publishes the session service, and registers capabilities.
156
+ *
157
+ * Always attempts detected servers unless they were explicitly disabled
158
+ * per language via `lsp.servers.<language>.enabled: false`.
159
+ * The global `lsp.enabled` and `lsp.active` keys are deprecated and ignored.
160
+ *
161
+ * Returns the start result and updates the controller's state.
162
+ */
163
+ async start(): Promise<LspStartResult> {
164
+ clearTsconfigCache();
165
+
166
+ // Restart safety: shut down any existing session before creating a new one
167
+ await this.cleanupExistingSession();
168
+
169
+ const lspSettings = loadLspSettings(this.#cwd);
170
+ // Note: lspSettings.enabled is ignored — the global switch is deprecated.
171
+ // Per-language `lsp.servers.<language>.enabled: false` is the supported
172
+ // way to opt out and is already handled by loadConfig.
173
+ // lspSettings.active is also ignored — the allowlist is deprecated.
174
+
175
+ const config = loadConfig(this.#cwd);
176
+
177
+ try {
178
+ return await this.initializeLspSession(config, lspSettings);
179
+ } catch (error: unknown) {
180
+ return this.setUnavailable(error);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Shut down any existing LSP session before starting a new one.
186
+ */
187
+ private async cleanupExistingSession(): Promise<void> {
188
+ this.#readinessGeneration++;
189
+ if (this.#state.kind !== "ready") return;
190
+ await this.#state.manager.shutdownAll();
191
+ if (this.#runtime) unregisterLspCapabilities(this.#runtime, this.#cwd);
192
+ clearSessionLspService(this.#cwd);
193
+ }
194
+
195
+ /** Set controller state to unavailable with the given error. */
196
+ private setUnavailable(error: unknown): LspStartResult {
197
+ const reason = error instanceof Error ? error.message : String(error);
198
+ this.#state = { kind: "unavailable", reason };
199
+ setSessionLspServiceState(this.#cwd, { kind: "unavailable", reason });
200
+ return { kind: "unavailable", reason };
201
+ }
202
+
203
+ /**
204
+ * Initialize the LSP session: create manager, detect and start servers,
205
+ * publish service state and capabilities.
206
+ */
207
+ private async initializeLspSession(
208
+ config: LspConfig,
209
+ settings: LspSettings,
210
+ ): Promise<LspStartResult> {
211
+ clearSessionLspService(this.#cwd);
212
+ this.#state = { kind: "pending" };
213
+
214
+ const manager = new LspManager(config, this.#cwd);
215
+ manager.setExcludePatterns(settings.exclude);
216
+ setSessionLspServiceState(this.#cwd, { kind: "pending" });
217
+
218
+ const detectedServers = scanProjectCapabilities(config, this.#cwd);
219
+ manager.registerDetectedServers(detectedServers);
220
+ await startDetectedServers(manager, detectedServers);
221
+
222
+ scanWorkspaceSentinels(this.#cwd);
223
+
224
+ const service = new SessionLspService(manager);
225
+ setSessionLspServiceState(this.#cwd, { kind: "ready", service });
226
+
227
+ if (this.#runtime) {
228
+ registerPendingLspCapabilities(this.#runtime, this.#cwd, service);
229
+ }
230
+
231
+ const projectServers = manager.getKnownProjectServers(detectedServers);
232
+
233
+ this.#state = {
234
+ kind: "ready",
235
+ manager,
236
+ service,
237
+ projectServers,
238
+ detectedServers,
239
+ settings,
240
+ };
241
+
242
+ const readinessGeneration = ++this.#readinessGeneration;
243
+ void this.promoteSemanticReadiness(manager, detectedServers, readinessGeneration);
244
+
245
+ return { kind: "ready", manager, service };
246
+ }
247
+
248
+ private async promoteSemanticReadiness(
249
+ manager: LspManager,
250
+ detectedServers: DetectedProjectServer[],
251
+ readinessGeneration: number,
252
+ ): Promise<void> {
253
+ try {
254
+ await manager.waitUntilWorkspaceReady();
255
+ } catch {
256
+ this.retractPendingCapabilities();
257
+ return;
258
+ }
259
+
260
+ if (
261
+ readinessGeneration !== this.#readinessGeneration ||
262
+ this.#state.kind !== "ready" ||
263
+ this.#state.manager !== manager
264
+ ) {
265
+ return;
266
+ }
267
+
268
+ this.#state.projectServers = manager.getKnownProjectServers(detectedServers);
269
+ if (this.#runtime) {
270
+ markLspCapabilitiesReady(this.#runtime, this.#cwd);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Retract the pending semantic registration when warm-up fails.
276
+ * Leaves the workspace in unavailable state so callers see a definitive
277
+ * failure rather than an orphaned pending capability.
278
+ */
279
+ private retractPendingCapabilities(): void {
280
+ if (this.#runtime) {
281
+ unregisterLspCapabilities(this.#runtime, this.#cwd);
282
+ }
283
+ setSessionLspServiceState(this.#cwd, {
284
+ kind: "unavailable",
285
+ reason: "LSP warm-up failed. Check code_health for server status.",
286
+ });
287
+ }
288
+
289
+ /**
290
+ * Shut down the LSP session.
291
+ *
292
+ * Unregisters capabilities, clears the service state, and shuts down
293
+ * all LSP clients.
294
+ */
295
+ async shutdown(): Promise<void> {
296
+ this.#readinessGeneration++;
297
+ clearTsconfigCache();
298
+
299
+ if (this.#runtime) {
300
+ unregisterLspCapabilities(this.#runtime, this.#cwd);
301
+ }
302
+
303
+ if (this.#cwd) {
304
+ clearSessionLspService(this.#cwd);
305
+ }
306
+
307
+ if (this.#state.kind === "ready") {
308
+ await this.#state.manager.shutdownAll();
309
+ }
310
+
311
+ this.#state = { kind: "initial" };
312
+ }
313
+
314
+ /** Get the missing servers warning (servers whose binary is not on PATH). */
315
+ getMissingServers(): Array<{ name: string; command: string }> {
316
+ if (this.#state.kind !== "ready") return [];
317
+ const config = loadConfig(this.#cwd);
318
+ return scanMissingServers(config, this.#cwd);
319
+ }
320
+ }
@@ -11,12 +11,28 @@ import { createLspSemanticProvider } from "../provider/lsp-semantic-provider.ts"
11
11
  import type { SessionLspService } from "./service-registry.ts";
12
12
 
13
13
  /**
14
- * Register LSP capabilities for a workspace cwd.
14
+ * Register LSP capabilities for a workspace cwd in pending state.
15
15
  *
16
- * Wraps SessionLspService into a SemanticProvider via the existing
17
- * semantic adapter and publishes it into the shared workspace runtime
18
- * so that code-intelligence and other consumers can discover semantic
19
- * analysis availability.
16
+ * Use this once the session service exists but before the language server has
17
+ * finished its initial readiness pipeline. Calls can then be deferred instead
18
+ * of silently observing "no provider" during startup.
19
+ */
20
+ export function registerPendingLspCapabilities(
21
+ runtime: WorkspaceRuntime,
22
+ cwd: string,
23
+ service: SessionLspService,
24
+ ): void {
25
+ const provider = createLspSemanticProvider(service);
26
+ runtime.registerSemanticPending(cwd, provider);
27
+ }
28
+
29
+ /**
30
+ * Register LSP capabilities for a workspace cwd as ready.
31
+ *
32
+ * Wraps SessionLspService into a SemanticProvider via the existing semantic
33
+ * adapter and publishes it into the shared workspace runtime so that
34
+ * code-intelligence and other consumers can discover semantic analysis
35
+ * availability.
20
36
  */
21
37
  export function registerLspCapabilities(
22
38
  runtime: WorkspaceRuntime,
@@ -27,6 +43,11 @@ export function registerLspCapabilities(
27
43
  runtime.registerSemantic(cwd, provider);
28
44
  }
29
45
 
46
+ /** Promote an already-registered pending semantic provider to ready. */
47
+ export function markLspCapabilitiesReady(runtime: WorkspaceRuntime, cwd: string): void {
48
+ runtime.markSemanticReady(cwd);
49
+ }
50
+
30
51
  /**
31
52
  * Unregister LSP capabilities for a workspace cwd.
32
53
  *