@mrclrchtr/supi-lsp 4.7.0 → 4.9.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 (74) hide show
  1. package/node_modules/@mrclrchtr/supi-code-runtime/README.md +1 -1
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -15
  3. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +1 -6
  4. package/node_modules/@mrclrchtr/supi-core/README.md +29 -35
  5. package/node_modules/@mrclrchtr/supi-core/package.json +4 -7
  6. package/node_modules/@mrclrchtr/supi-core/src/api.ts +4 -6
  7. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +0 -20
  8. package/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +7 -1
  9. package/node_modules/@mrclrchtr/supi-core/src/config.ts +0 -1
  10. package/node_modules/@mrclrchtr/supi-core/src/context.ts +1 -9
  11. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +8 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +107 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/index.ts +4 -6
  14. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +54 -28
  15. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +91 -125
  16. package/node_modules/@mrclrchtr/supi-core/src/settings.ts +10 -7
  17. package/package.json +3 -3
  18. package/src/client/client-diagnostics.ts +361 -0
  19. package/src/client/client.ts +46 -238
  20. package/src/manager/manager.ts +25 -33
  21. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md +0 -120
  22. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +0 -75
  23. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +0 -42
  24. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config.ts +0 -221
  25. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +0 -357
  26. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config.ts +0 -11
  27. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +0 -119
  28. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +0 -36
  29. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +0 -31
  30. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context.ts +0 -16
  31. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -287
  32. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +0 -41
  33. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +0 -57
  34. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +0 -36
  35. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/llm.ts +0 -201
  36. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +0 -134
  37. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +0 -44
  38. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/path.ts +0 -2
  39. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +0 -189
  40. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +0 -170
  41. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/project.ts +0 -15
  42. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +0 -4
  43. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +0 -93
  44. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/report.ts +0 -121
  45. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +0 -71
  46. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/session.ts +0 -8
  47. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +0 -373
  48. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +0 -102
  49. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +0 -15
  50. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +0 -76
  51. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +0 -487
  52. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +0 -141
  53. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +0 -118
  54. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +0 -3
  55. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings.ts +0 -33
  56. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +0 -11
  57. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +0 -68
  58. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/terminal.ts +0 -60
  59. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +0 -192
  60. package/node_modules/@mrclrchtr/supi-code-runtime/src/index.ts +0 -42
  61. package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/context.ts +0 -45
  62. package/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +0 -119
  63. package/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +0 -189
  64. package/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +0 -373
  65. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +0 -102
  66. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +0 -15
  67. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +0 -141
  68. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +0 -118
  69. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +0 -3
  70. package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +0 -192
  71. package/src/client/client-refresh.ts +0 -234
  72. package/src/diagnostics/diagnostics.ts +0 -99
  73. package/src/diagnostics/suppression-diagnostics.ts +0 -60
  74. package/src/index.ts +0 -41
@@ -1,9 +1,8 @@
1
1
  // LSP Client — wraps a server process + JsonRpcClient.
2
2
  // Handles initialize handshake, document sync, shutdown, and crash recovery.
3
3
 
4
- // biome-ignore lint/style/noExcessiveLinesPerFile: LspClient remains a cohesive stateful wrapper; refresh logic is already split out.
4
+ // biome-ignore lint/style/noExcessiveLinesPerFile: process lifecycle, readiness, and protocol requests stay in one client wrapper; document and diagnostic state is delegated.
5
5
  import { type ChildProcess, execSync, spawn } from "node:child_process";
6
- import { existsSync } from "node:fs";
7
6
  import * as path from "node:path";
8
7
  import {
9
8
  type CodeQueryResult,
@@ -18,6 +17,7 @@ import type {
18
17
  CodeActionContext,
19
18
  Diagnostic,
20
19
  DidChangeWatchedFilesParams,
20
+ DocumentDiagnosticReport,
21
21
  DocumentSymbol,
22
22
  FileEvent,
23
23
  Hover,
@@ -30,17 +30,14 @@ import type {
30
30
  ServerCapabilities,
31
31
  ServerConfig,
32
32
  SymbolInformation,
33
- TextDocumentIdentifier,
34
- TextDocumentItem,
35
- VersionedTextDocumentIdentifier,
36
33
  WorkspaceEdit,
37
34
  WorkspaceSymbol,
38
35
  } from "../config/types.ts";
39
- import { detectLanguageId, fileToUri, uriToFile } from "../utils.ts";
36
+ import { fileToUri } from "../utils.ts";
37
+ import { ClientDiagnostics, type DiagnosticEntry } from "./client-diagnostics.ts";
40
38
  import { JsonRpcClient, JsonRpcRequestError } from "./transport.ts";
41
39
 
42
40
  const SHUTDOWN_TIMEOUT_MS = 5_000;
43
- const DIAGNOSTIC_WAIT_MS = 3_000;
44
41
 
45
42
  /** Race an operation against a timeout without retaining the timer after settlement. */
46
43
  async function withTimeout<T>(
@@ -89,19 +86,6 @@ function killProcessTree(pid: number): void {
89
86
  // ── Types ─────────────────────────────────────────────────────────────
90
87
  export type ClientStatus = "initializing" | "running" | "error" | "shutdown";
91
88
 
92
- export interface DiagnosticEntry {
93
- uri: string;
94
- diagnostics: Diagnostic[];
95
- }
96
-
97
- /** Internal metadata tracked alongside cached diagnostics. */
98
- export interface DiagnosticCacheEntry {
99
- diagnostics: Diagnostic[];
100
- receivedAt: number;
101
- version?: number;
102
- resultId?: string;
103
- }
104
-
105
89
  // ── LspClient ─────────────────────────────────────────────────────────
106
90
  export class LspClient {
107
91
  readonly name: string;
@@ -111,13 +95,7 @@ export class LspClient {
111
95
  private rpc: JsonRpcClient | null = null;
112
96
  private _status: ClientStatus = "initializing";
113
97
  private capabilities: ServerCapabilities | null = null;
114
-
115
- /** Open documents: uri → { version, languageId } */
116
- private openDocs = new Map<string, { version: number; languageId: string }>();
117
- /** Per-file diagnostics with freshness metadata */
118
- private diagnosticStore = new Map<string, DiagnosticCacheEntry>();
119
- /** Listeners waiting for diagnostics on a specific uri */
120
- private diagnosticWaiters = new Map<string, Array<() => void>>();
98
+ private readonly diagnostics: ClientDiagnostics;
121
99
 
122
100
  // ── Readiness (work-done-progress) ──────────────────────────────────
123
101
  private trackedTokens = new Map<ProgressToken, "begin-seen" | "ended">();
@@ -135,6 +113,23 @@ export class LspClient {
135
113
  ) {
136
114
  this.name = name;
137
115
  this.root = root;
116
+ this.diagnostics = new ClientDiagnostics({
117
+ isOperational: () => this.rpc !== null && this._status === "running",
118
+ supportsPullDiagnostics: () => this.hasDiagnosticProvider,
119
+ sendNotification: (method, params) => {
120
+ if (this.rpc) void this.rpc.sendNotification(method, params);
121
+ },
122
+ pullDocumentDiagnostics: async (uri, previousResultId, timeoutMs) => {
123
+ const rpc = this.rpc;
124
+ if (!rpc || this._status !== "running") throw new Error("client not running");
125
+ await this.getReady();
126
+ return rpc.sendRequest(
127
+ "textDocument/diagnostic",
128
+ { textDocument: { uri }, previousResultId },
129
+ { timeoutMs },
130
+ ) as Promise<DocumentDiagnosticReport>;
131
+ },
132
+ });
138
133
  }
139
134
 
140
135
  get status(): ClientStatus {
@@ -142,7 +137,7 @@ export class LspClient {
142
137
  }
143
138
 
144
139
  get openFiles(): string[] {
145
- return Array.from(this.openDocs.keys()).map(uriToFile);
140
+ return this.diagnostics.openFiles;
146
141
  }
147
142
 
148
143
  get serverCapabilities(): ServerCapabilities | null {
@@ -277,9 +272,7 @@ export class LspClient {
277
272
  }
278
273
  }
279
274
 
280
- this.openDocs.clear();
281
- this.diagnosticStore.clear();
282
- this.releaseAllDiagnosticWaiters();
275
+ this.diagnostics.clear();
283
276
 
284
277
  // Clear readiness state
285
278
  if (this.noProgressTimer) {
@@ -291,126 +284,40 @@ export class LspClient {
291
284
  this.rejectReady(new Error("Client shutdown"));
292
285
  }
293
286
 
294
- // ── Document Synchronization ────────────────────────────────────────
295
- /** Open a document (or re-sync if already open). */
287
+ // ── Document Synchronization and Diagnostics ────────────────────────
288
+ /** Open a document, or update it when it is already open. */
296
289
  didOpen(filePath: string, content: string): void {
297
- if (!this.rpc || this._status !== "running") return;
298
-
299
- const uri = fileToUri(filePath);
300
- const languageId = detectLanguageId(filePath);
301
-
302
- if (this.openDocs.has(uri)) {
303
- // Already open — send didChange instead
304
- this.didChange(filePath, content);
305
- return;
306
- }
307
-
308
- this.openDocs.set(uri, { version: 1, languageId });
309
- void this.rpc.sendNotification("textDocument/didOpen", {
310
- textDocument: {
311
- uri,
312
- languageId,
313
- version: 1,
314
- text: content,
315
- } satisfies TextDocumentItem,
316
- });
290
+ this.diagnostics.didOpen(filePath, content);
317
291
  }
318
292
 
319
- /** Notify the server of a content change (full document sync). */
293
+ /** Update a document, or open it when it is not tracked yet. */
320
294
  didChange(filePath: string, content: string): void {
321
- if (!this.rpc || this._status !== "running") return;
322
-
323
- const uri = fileToUri(filePath);
324
- const doc = this.openDocs.get(uri);
325
-
326
- if (!doc) {
327
- // Not yet open — do a didOpen
328
- this.didOpen(filePath, content);
329
- return;
330
- }
331
-
332
- doc.version++;
333
- void this.rpc.sendNotification("textDocument/didChange", {
334
- textDocument: { uri, version: doc.version } satisfies VersionedTextDocumentIdentifier,
335
- contentChanges: [{ text: content }],
336
- });
295
+ this.diagnostics.didChange(filePath, content);
337
296
  }
338
297
 
339
- /** Close a document and clear any cached state for it. */
298
+ /** Close a document and remove its cached diagnostic state. */
340
299
  didClose(filePath: string): void {
341
- const uri = fileToUri(filePath);
342
- const wasOpen = this.openDocs.has(uri);
343
-
344
- this.clearFileState(uri);
345
-
346
- if (!wasOpen || !this.rpc || this._status !== "running") return;
347
-
348
- void this.rpc.sendNotification("textDocument/didClose", {
349
- textDocument: { uri } satisfies TextDocumentIdentifier,
350
- });
300
+ this.diagnostics.didClose(filePath);
351
301
  }
352
302
 
353
- /** Prune missing files from open documents and cached diagnostics. */
303
+ /** Remove missing document and diagnostic state, and return the removed paths. */
354
304
  pruneMissingFiles(): string[] {
355
- const uris = new Set([...this.openDocs.keys(), ...this.diagnosticStore.keys()]);
356
- const removedFiles: string[] = [];
357
-
358
- for (const uri of uris) {
359
- const filePath = uriToFile(uri);
360
- if (existsSync(filePath)) continue;
361
-
362
- const wasOpen = this.openDocs.has(uri);
363
- this.clearFileState(uri);
364
- removedFiles.push(filePath);
365
-
366
- if (wasOpen && this.rpc && this._status === "running") {
367
- void this.rpc.sendNotification("textDocument/didClose", {
368
- textDocument: { uri } satisfies TextDocumentIdentifier,
369
- });
370
- }
371
- }
372
-
373
- return removedFiles;
305
+ return this.diagnostics.pruneMissingFiles();
374
306
  }
375
307
 
376
- // ── Diagnostics ─────────────────────────────────────────────────────
377
- /** Get stored diagnostics for a file. */
308
+ /** Return stored diagnostics for one file. */
378
309
  getDiagnostics(filePath: string): Diagnostic[] {
379
- return this.diagnosticStore.get(fileToUri(filePath))?.diagnostics ?? [];
310
+ return this.diagnostics.getDiagnostics(filePath);
380
311
  }
381
312
 
382
- /**
383
- * Get all stored diagnostics across all files.
384
- *
385
- * Filters out empty entries and — defensively — files that no longer exist
386
- * on disk. The latter guards against a race where a `publishDiagnostics`
387
- * notification (already in-flight) arrives *after* `pruneMissingFiles()`
388
- * has removed the file from `openDocs`, recreating a stale cache entry.
389
- */
313
+ /** Return non-empty diagnostics for files that still exist. */
390
314
  getAllDiagnostics(): DiagnosticEntry[] {
391
- const result: DiagnosticEntry[] = [];
392
- for (const [uri, entry] of this.diagnosticStore) {
393
- if (entry.diagnostics.length === 0) continue;
394
- // Defensive: drop diagnostics for files deleted after prune. Late
395
- // in-flight publishDiagnostics notifications can recreate stale entries.
396
- if (!existsSync(uriToFile(uri))) continue;
397
- result.push({ uri, diagnostics: entry.diagnostics });
398
- }
399
- return result;
315
+ return this.diagnostics.getAllDiagnostics();
400
316
  }
401
317
 
402
- /**
403
- * Clear all pull-diagnostic result IDs, forcing full (not `unchanged`)
404
- * pull diagnostic responses on the next refresh cycle.
405
- *
406
- * Use this after file creation/write operations so that cross-file
407
- * diagnostics (e.g., `Cannot find module` errors in importing files)
408
- * are fully re-computed instead of returned as `unchanged`.
409
- */
318
+ /** Force the next pull refresh to request complete diagnostic reports. */
410
319
  clearPullResultIds(): void {
411
- for (const entry of this.diagnosticStore.values()) {
412
- delete entry.resultId;
413
- }
320
+ this.diagnostics.clearPullResultIds();
414
321
  }
415
322
 
416
323
  /** Check if server supports pull diagnostics. */
@@ -426,46 +333,16 @@ export class LspClient {
426
333
  } satisfies DidChangeWatchedFilesParams);
427
334
  }
428
335
 
429
- /**
430
- * Re-read and re-sync all currently open, existing documents.
431
- * Delegates to client-refresh module.
432
- */
336
+ /** Re-read open documents, then collect pull diagnostics or wait for push diagnostics. */
433
337
  async refreshOpenDiagnostics(
434
338
  options: { maxWaitMs?: number; quietMs?: number } = {},
435
339
  ): Promise<void> {
436
- const { refreshClientOpenDiagnostics } = await import("./client-refresh.ts");
437
- return refreshClientOpenDiagnostics(this, options);
340
+ return this.diagnostics.refreshOpenDiagnostics(options);
438
341
  }
439
342
 
440
- /**
441
- * Sync a file and wait for diagnostics (up to timeout).
442
- * Returns diagnostics for the file.
443
- */
343
+ /** Sync one file and return its diagnostics after pull or push collection. */
444
344
  async syncAndWaitForDiagnostics(filePath: string, content: string): Promise<Diagnostic[]> {
445
- const uri = fileToUri(filePath);
446
- const syncStart = Date.now();
447
-
448
- // Sync the content
449
- this.didChange(filePath, content);
450
-
451
- // Prefer pull diagnostics when available, but fall back to push notifications.
452
- if (this.hasDiagnosticProvider) {
453
- const remaining = DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart);
454
- if (remaining > 0) {
455
- try {
456
- const { pullDiagnosticsForUri } = await import("./client-refresh.ts");
457
- const pulled = await pullDiagnosticsForUri(this, uri, remaining);
458
- if (pulled) {
459
- return this.getDiagnostics(filePath);
460
- }
461
- } catch {
462
- // Pull diagnostics failed — fall back to push wait
463
- }
464
- }
465
- }
466
-
467
- await this.waitForDiagnostics(uri, Math.max(0, DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart)));
468
- return this.getDiagnostics(filePath);
345
+ return this.diagnostics.syncAndWaitForDiagnostics(filePath, content);
469
346
  }
470
347
 
471
348
  // ── LSP Requests ───────────────────────────────────────────────────
@@ -611,78 +488,9 @@ export class LspClient {
611
488
  return items.map(() => null);
612
489
  }
613
490
 
614
- private handlePublishDiagnostics(params: PublishDiagnosticsParams): void {
615
- // If the publication includes a version and we have a newer synced
616
- // version for this open document, ignore the stale publication.
617
- if (params.version !== undefined && params.version !== null) {
618
- const openDoc = this.openDocs.get(params.uri);
619
- if (openDoc && params.version < openDoc.version) {
620
- return;
621
- }
622
- }
623
-
624
- this.diagnosticStore.set(params.uri, {
625
- diagnostics: params.diagnostics,
626
- receivedAt: Date.now(),
627
- version: params.version ?? undefined,
628
- });
629
- this.releaseDiagnosticWaiters(params.uri);
630
- }
631
-
632
- /** Wait for diagnostics on a URI, resolving on publication or timeout. */
633
- private waitForDiagnostics(uri: string, timeoutMs: number): Promise<void> {
634
- if (timeoutMs <= 0) return Promise.resolve();
635
-
636
- return new Promise<void>((resolve) => {
637
- const waiter = () => {
638
- clearTimeout(timer);
639
- this.removeDiagnosticWaiter(uri, waiter);
640
- resolve();
641
- };
642
- const timer = setTimeout(() => {
643
- this.removeDiagnosticWaiter(uri, waiter);
644
- resolve();
645
- }, timeoutMs);
646
- const waiters = this.diagnosticWaiters.get(uri) ?? [];
647
- waiters.push(waiter);
648
- this.diagnosticWaiters.set(uri, waiters);
649
- });
650
- }
651
-
652
- /** Clear all per-file state and wake any diagnostics callers waiting on this URI. */
653
- private clearFileState(uri: string): void {
654
- this.openDocs.delete(uri);
655
- this.diagnosticStore.delete(uri);
656
- this.releaseDiagnosticWaiters(uri);
657
- }
658
-
659
- /** Remove a single pending diagnostics waiter, usually after its timeout fires. */
660
- private removeDiagnosticWaiter(uri: string, waiter: () => void): void {
661
- const waiters = this.diagnosticWaiters.get(uri);
662
- if (!waiters) return;
663
-
664
- const next = waiters.filter((entry) => entry !== waiter);
665
- if (next.length > 0) {
666
- this.diagnosticWaiters.set(uri, next);
667
- } else {
668
- this.diagnosticWaiters.delete(uri);
669
- }
670
- }
671
-
672
- /** Wake every pending diagnostics waiter during shutdown or bulk cleanup. */
673
- private releaseAllDiagnosticWaiters(): void {
674
- for (const uri of Array.from(this.diagnosticWaiters.keys())) {
675
- this.releaseDiagnosticWaiters(uri);
676
- }
677
- }
678
-
679
- /** Wake all diagnostics waiters for a URI and remove them from the waiter map. */
680
- private releaseDiagnosticWaiters(uri: string): void {
681
- const waiters = this.diagnosticWaiters.get(uri);
682
- if (!waiters) return;
683
-
684
- this.diagnosticWaiters.delete(uri);
685
- for (const waiter of waiters) waiter();
491
+ /** Apply a diagnostic publication received from the LSP transport. */
492
+ handlePublishDiagnostics(params: PublishDiagnosticsParams): void {
493
+ this.diagnostics.handlePublishDiagnostics(params);
686
494
  }
687
495
 
688
496
  // ── Readiness (work-done-progress) ──────────────────────────────────
@@ -12,6 +12,7 @@ import type {
12
12
  FileEvent,
13
13
  LspConfig,
14
14
  ProjectServerInfo,
15
+ ServerConfig,
15
16
  SymbolInformation,
16
17
  WorkspaceSymbol,
17
18
  } from "../config/types.ts";
@@ -65,6 +66,13 @@ import {
65
66
 
66
67
  type UnavailableReason = "missing-command" | "start-failed" | "runtime-error";
67
68
 
69
+ type FileRoute = {
70
+ serverName: string;
71
+ serverConfig: ServerConfig;
72
+ root: string;
73
+ key: string;
74
+ };
75
+
68
76
  // ── LspManager ────────────────────────────────────────────────────────
69
77
  export class LspManager {
70
78
  /** Active clients keyed by "serverName:root" */
@@ -96,30 +104,16 @@ export class LspManager {
96
104
  this.excludePatterns = patterns;
97
105
  }
98
106
 
99
- /** Check whether any configured language server handles the given file's extension. */
100
- hasServerForExtension(filePath: string): boolean {
101
- return getServerForFile(this.config, resolveSessionPath(this.cwd, filePath)) !== null;
102
- }
103
107
  // ── Public API ────────────────────────────────────────────────────
104
108
  registerDetectedServers(detected: DetectedProjectServer[]): void {
105
109
  this.knownRoots = projectRoots.buildKnownRootsMap(detected);
106
110
  }
107
111
  /** Check whether a file path has an available LSP server for explicit semantic operations. */
108
112
  canServeFile(filePath: string): boolean {
109
- const resolvedPath = resolveSessionPath(this.cwd, filePath);
110
- const match = getServerForFile(this.config, resolvedPath);
111
- if (!match) return false;
112
- const [serverName, serverConfig] = match;
113
- // Mirror getClientForFile's root resolution so the unavailable check stays
114
- // root-specific. A failed startup in one workspace must not suppress
115
- // activation for unrelated roots served by the same language server.
116
- const root = resolveRootForFile(resolvedPath, serverName, serverConfig.rootMarkers, {
117
- knownRoots: this.knownRoots,
118
- cwd: this.cwd,
119
- });
120
- const key = clientKey(serverName, root);
121
- if (this.getUnavailableReason(key, serverConfig.command)) return false;
122
- return this.isServerCommandAvailable(serverConfig.command);
113
+ const route = this.resolveFileRoute(filePath);
114
+ if (!route) return false;
115
+ if (this.getUnavailableReason(route.key, route.serverConfig.command)) return false;
116
+ return this.isServerCommandAvailable(route.serverConfig.command);
123
117
  }
124
118
 
125
119
  /**
@@ -160,15 +154,8 @@ export class LspManager {
160
154
  }
161
155
  /** Get or create an LSP client for the given file. */
162
156
  async getClientForFile(filePath: string): Promise<LspClient | null> {
163
- const resolvedPath = resolveSessionPath(this.cwd, filePath);
164
- const match = getServerForFile(this.config, resolvedPath);
165
- if (!match) return null;
166
- const [serverName, serverConfig] = match;
167
- const root = resolveRootForFile(resolvedPath, serverName, serverConfig.rootMarkers, {
168
- knownRoots: this.knownRoots,
169
- cwd: this.cwd,
170
- });
171
- return this.startServerForRoot(serverName, root);
157
+ const route = this.resolveFileRoute(filePath);
158
+ return route ? this.startServerForRoot(route.serverName, route.root) : null;
172
159
  }
173
160
  async startServerForRoot(serverName: string, root: string): Promise<LspClient | null> {
174
161
  const serverConfig = this.config.servers[serverName];
@@ -213,7 +200,7 @@ export class LspManager {
213
200
  */
214
201
  private async performStart(
215
202
  serverName: string,
216
- serverConfig: import("../config/types.ts").ServerConfig,
203
+ serverConfig: ServerConfig,
217
204
  root: string,
218
205
  key: string,
219
206
  ): Promise<LspClient | null> {
@@ -241,8 +228,8 @@ export class LspManager {
241
228
  }
242
229
  }
243
230
 
244
- /** Find an already-started client for a file without spawning a new server. */
245
- private getExistingClientForFile(filePath: string): LspClient | null {
231
+ /** Resolve a file to its configured server and workspace-specific client key. */
232
+ private resolveFileRoute(filePath: string): FileRoute | null {
246
233
  const resolvedPath = resolveSessionPath(this.cwd, filePath);
247
234
  const match = getServerForFile(this.config, resolvedPath);
248
235
  if (!match) return null;
@@ -251,7 +238,13 @@ export class LspManager {
251
238
  knownRoots: this.knownRoots,
252
239
  cwd: this.cwd,
253
240
  });
254
- return this.clients.get(clientKey(serverName, root)) ?? null;
241
+ return { serverName, serverConfig, root, key: clientKey(serverName, root) };
242
+ }
243
+
244
+ /** Find an already-started client for a file without spawning a new server. */
245
+ private getExistingClientForFile(filePath: string): LspClient | null {
246
+ const route = this.resolveFileRoute(filePath);
247
+ return route ? (this.clients.get(route.key) ?? null) : null;
255
248
  }
256
249
 
257
250
  /** Restart the clients that own the supplied file paths, if any are active. */
@@ -260,8 +253,7 @@ export class LspManager {
260
253
  const seen = new Set<string>();
261
254
 
262
255
  for (const filePath of filePaths) {
263
- const resolvedPath = resolveSessionPath(this.cwd, filePath);
264
- const client = this.getExistingClientForFile(resolvedPath);
256
+ const client = this.getExistingClientForFile(filePath);
265
257
  if (!client) continue;
266
258
 
267
259
  const key = clientKey(client.name, client.root);
@@ -1,120 +0,0 @@
1
- <div align="center">
2
- <a href="https://github.com/mrclrchtr/supi/tree/main/packages/supi-core">
3
- <img src="https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-core/assets/social-preview.png" alt="SuPi Core" width="100%">
4
- </a>
5
- </div>
6
-
7
- # @mrclrchtr/supi-core
8
-
9
- Shared infrastructure for SuPi extensions.
10
-
11
- This is a **pure library** — it does not register any pi commands or tools. The `/supi-settings` command is now available through `@mrclrchtr/supi-settings`.
12
-
13
- ## Install
14
-
15
- ```bash
16
- pnpm add @mrclrchtr/supi-core
17
- ```
18
-
19
- ## Package surfaces
20
-
21
- - `@mrclrchtr/supi-core/api` — reusable helpers for other packages and extensions
22
- - `@mrclrchtr/supi-core/report` — shared text/report rendering helpers for TUI and plain-text summaries
23
-
24
- ## What you get from the API
25
-
26
- ### Config helpers
27
-
28
- - `loadSupiConfig()` — merged config with resolution order `defaults <- global <- project`
29
- - `loadSupiConfigForScope()` — load one scope at a time for settings UIs
30
- - `writeSupiConfig()` — persist values
31
- - `removeSupiConfigKey()` — remove a key or override
32
-
33
- Config file locations:
34
-
35
- - global: `~/.pi/agent/supi/config.json`
36
- - project: `.pi/supi/config.json`
37
-
38
- ### Settings helpers
39
-
40
- - `registerDeclarativeSettings(pi, options)` — contribute a config-backed declarative settings section with source-aware scoped persistence
41
- - `registerSettingsCommand(pi)` — register `/supi-settings` (used by `@mrclrchtr/supi-settings`)
42
- - `openSettingsOverlay(pi, ctx)` — open the shared settings UI directly
43
- - `createInputSubmenu()` — helper for simple text-entry submenus
44
- - `createModelPickerSubmenu()` — helper for scoped model selection submenus, with optional host-owned choices and `disabled` control
45
-
46
- The built-in settings UI supports:
47
-
48
- - project/global scope toggle
49
- - source badges for project, global, and default values
50
- - Inherit/Reset actions that delete scoped config keys
51
- - grouped extension sections
52
- - searchable setting lists
53
-
54
- ### Context helpers
55
-
56
- - `wrapExtensionContext()` — wrap injected text in SuPi's `<extension-context>` tag
57
- - `findLastUserMessageIndex()`
58
- - `getContextToken()`
59
- - `getPromptContent()`
60
- - `pruneAndReorderContextMessages()`
61
- - `restorePromptContent()`
62
-
63
- ### Shared registries
64
-
65
- - context-provider registry for `/supi-context`
66
- - debug-event registry for producers that want shared debug capture
67
- - settings registry used by `/supi-settings`
68
-
69
- ### Project and session helpers
70
-
71
- - project-root detection and directory walking helpers such as `findProjectRoot()` and `walkProject()`
72
- - active-branch session helper: `getActiveBranchEntries()`
73
- - terminal helpers such as `formatTitle()`, `signalWaiting()`, and `signalDone()`
74
-
75
- ### Report helpers
76
-
77
- - `clampReportWidth()` — enforce a minimum readable report width
78
- - `formatReportTitle()` / `formatSectionHeader()` — shared themed headers
79
- - `formatDimLine()` / `formatKeyValueLine()` — common summary rows
80
- - `formatOverflowHint()` — consistent preview-overflow hints
81
- - `wrapReportText()` — ANSI-aware wrapped report blocks with optional indentation
82
-
83
- ## Example
84
-
85
- ```ts
86
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
87
- import { loadSupiConfig, registerDeclarativeSettings, wrapExtensionContext } from "@mrclrchtr/supi-core/api";
88
-
89
- export default function myExtension(pi: ExtensionAPI) {
90
- const defaults = { enabled: true };
91
- const config = loadSupiConfig("my-extension", process.cwd(), defaults);
92
-
93
- registerDeclarativeSettings(pi, {
94
- id: "my-extension",
95
- label: "My Extension",
96
- section: "my-extension",
97
- defaults,
98
- fields: [
99
- {
100
- kind: "boolean" as const,
101
- key: "enabled",
102
- label: "Enabled",
103
- },
104
- ],
105
- });
106
-
107
- const message = wrapExtensionContext("my-extension", "hello", {
108
- enabled: config.enabled,
109
- });
110
- void message;
111
- }
112
- ```
113
-
114
- ## Source
115
-
116
- - `src/api.ts` — exported library surface
117
- - `src/config.ts` — shared config loading and writing
118
- - `src/config-settings.ts` — config-backed settings registration helper
119
- - `src/settings-ui.ts` — shared settings overlay
120
- - `src/report.ts` — shared text/report rendering helpers