@mrclrchtr/supi-code-intelligence 6.4.0 → 7.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 (129) hide show
  1. package/README.md +164 -82
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  4. package/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  5. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  6. package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  7. package/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  9. package/node_modules/@mrclrchtr/supi-lsp/README.md +35 -13
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/index.js +3 -1
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/legacy.js +3 -1
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/package.json +1 -1
  20. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  21. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +6 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +43 -35
  23. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +83 -17
  24. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +108 -92
  25. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +44 -82
  26. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +3 -4
  27. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +9 -68
  28. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +45 -58
  29. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +230 -331
  30. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +201 -1
  31. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +129 -70
  32. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-typescript.ts +395 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +8 -12
  34. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +685 -183
  35. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +2 -2
  36. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +60 -60
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-request-enrollment.ts +75 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-barrier.ts +443 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-errors.ts +30 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-file.ts +15 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +290 -37
  42. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +72 -16
  43. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +1 -0
  44. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +30 -4
  45. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +1 -1
  46. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +10 -4
  47. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-process-crash-report.ts +50 -1
  48. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +11 -7
  49. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +608 -45
  50. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +3 -1
  51. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +37 -4
  52. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +31 -11
  53. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +19 -3
  54. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  55. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  56. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  57. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  58. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  59. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  60. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  61. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
  62. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +2 -0
  63. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callee-display-name.ts +165 -0
  64. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +67 -9
  65. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +2 -0
  66. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
  67. package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
  68. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  69. package/node_modules/yaml/dist/nodes/Alias.js +25 -23
  70. package/node_modules/yaml/package.json +1 -1
  71. package/package.json +5 -5
  72. package/src/analysis/health/diagnostics.ts +1 -2
  73. package/src/analysis/health/file-scope.ts +4 -1
  74. package/src/analysis/health/server-status.ts +1 -1
  75. package/src/analysis/relations/callees.ts +17 -8
  76. package/src/analysis/relations/implementations.ts +1 -2
  77. package/src/analysis/relations/references.ts +8 -4
  78. package/src/analysis/relations/types.ts +6 -30
  79. package/src/analysis/target/anchored.ts +9 -1
  80. package/src/analysis/target/symbol.ts +29 -11
  81. package/src/analysis/target/types.ts +14 -6
  82. package/src/api.ts +2 -0
  83. package/src/session/graph/collect.ts +18 -3
  84. package/src/session/health-refresh.ts +12 -1
  85. package/src/session/health-types.ts +18 -6
  86. package/src/session/health-workflow.ts +21 -19
  87. package/src/session/orientation-types.ts +5 -6
  88. package/src/session/orientation-workflow.ts +10 -2
  89. package/src/session/target-workflow.ts +9 -6
  90. package/src/substrate/lsp/source-tracking.ts +11 -6
  91. package/src/tool/code_find/guidance.ts +2 -2
  92. package/src/tool/code_find/spec.ts +5 -4
  93. package/src/tool/code_find/tui.ts +7 -1
  94. package/src/tool/code_graph/details.ts +32 -0
  95. package/src/tool/code_graph/display.ts +54 -0
  96. package/src/tool/code_graph/format.ts +60 -0
  97. package/src/tool/code_graph/guidance.ts +1 -1
  98. package/src/tool/code_graph/markdown.ts +76 -198
  99. package/src/tool/code_graph/read-next.ts +65 -0
  100. package/src/tool/code_graph/result.ts +118 -83
  101. package/src/tool/code_graph/tui.ts +216 -54
  102. package/src/tool/code_health/guidance.ts +2 -4
  103. package/src/tool/code_health/markdown.ts +75 -58
  104. package/src/tool/code_health/refresh-outcome.ts +41 -1
  105. package/src/tool/code_health/refresh-status.ts +114 -48
  106. package/src/tool/code_health/scope.ts +78 -0
  107. package/src/tool/code_health/spec.ts +1 -1
  108. package/src/tool/code_health/tui.ts +25 -38
  109. package/src/tool/code_inspect/tui.ts +7 -1
  110. package/src/tool/code_orientation/result.ts +31 -8
  111. package/src/tool/code_orientation/tui.ts +26 -5
  112. package/src/tool/code_refactor_apply/guidance.ts +1 -1
  113. package/src/tool/code_refactor_apply/tui.ts +7 -3
  114. package/src/tool/code_refactor_plan/guidance.ts +1 -1
  115. package/src/tool/code_refactor_plan/tui.ts +7 -1
  116. package/src/tool/code_resolve/guidance.ts +2 -4
  117. package/src/tool/code_resolve/result.ts +13 -10
  118. package/src/tool/code_resolve/tui.ts +18 -1
  119. package/src/tool/infra/truncate.ts +39 -13
  120. package/src/tool/result/candidate-row.ts +27 -0
  121. package/src/tool/result/errors.ts +21 -6
  122. package/src/tool/result/types.ts +7 -3
  123. package/src/tool/schemas.ts +5 -0
  124. package/src/types/execution.ts +2 -0
  125. package/src/types/index.ts +2 -0
  126. package/src/ui/tui/candidate-legacy-body.ts +69 -0
  127. package/src/ui/tui/candidate-selection.ts +366 -0
  128. package/src/ui/tui/common.ts +26 -31
  129. package/src/ui/tui/execution-error.ts +92 -0
@@ -45,14 +45,31 @@ import { raceRequestControl } from "../session/readiness.ts";
45
45
  import {
46
46
  ClientDynamicRegistrations,
47
47
  DOCUMENT_DIAGNOSTIC_METHOD,
48
+ isDocumentSelectorApplicable,
48
49
  isValidDiagnosticOptions,
49
50
  } from "./client-diagnostic-capabilities.ts";
51
+ import type {
52
+ DiagnosticPullRequest,
53
+ DiagnosticRequestAdapter,
54
+ DiagnosticRequestExecution,
55
+ } from "./client-diagnostic-request.ts";
56
+ import { createPriorityDiagnosticRequestAdapter } from "./client-diagnostic-request.ts";
57
+ import { createTypeScriptDiagnosticRequestAdapter } from "./client-diagnostic-typescript.ts";
50
58
  import { ClientDiagnostics } from "./client-diagnostics.ts";
51
59
  import type { ClientDiagnosticSnapshot, DiagnosticEntry } from "./client-document-state.ts";
60
+ import { SemanticInputRequestRetryGuard } from "./client-request-enrollment.ts";
61
+ import {
62
+ isSemanticInputEnrollmentError,
63
+ SemanticInputSynchronizationError,
64
+ } from "./client-semantic-input-errors.ts";
52
65
  import { JsonRpcClient, JsonRpcRequestError } from "./transport.ts";
53
66
 
54
67
  const SHUTDOWN_TIMEOUT_MS = 5_000;
55
68
 
69
+ interface SemanticRequestAttemptState {
70
+ inputRevision: number | undefined;
71
+ }
72
+
56
73
  /**
57
74
  * Fixed bound after which a running client that never became ready is
58
75
  * considered readiness-stalled and eligible for a recovery restart.
@@ -176,6 +193,7 @@ export class LspClient {
176
193
  private rpc: JsonRpcClient | null = null;
177
194
  private _status: ClientStatus = "initializing";
178
195
  private capabilities: ServerCapabilities | null = null;
196
+ private readonly diagnosticRequestAdapter: DiagnosticRequestAdapter;
179
197
  private readonly diagnostics: ClientDiagnostics;
180
198
  /** Dynamic capability registrations for this client instance only. */
181
199
  private readonly dynamicRegistrations = new ClientDynamicRegistrations();
@@ -205,36 +223,41 @@ export class LspClient {
205
223
  ) {
206
224
  this.name = name;
207
225
  this.root = root;
226
+ const nativeDiagnosticAdapter: DiagnosticRequestAdapter = {
227
+ supports: (uri) => this.hasApplicableDiagnosticProvider(uri),
228
+ sourceFor: (uri) => (this.hasApplicableDiagnosticProvider(uri) ? "pull" : undefined),
229
+ collect: (request) => this.startNativeDiagnosticRequest(request),
230
+ };
231
+ const typescriptDiagnosticAdapter = createTypeScriptDiagnosticRequestAdapter({
232
+ fileTypes: config.fileTypes,
233
+ cwd: cwd ?? root,
234
+ isSupportedRoute: () => this.isSupportedTypeScriptRoute(),
235
+ hasCommand: () => this.hasTypeScriptRequestCommand(),
236
+ getReady: () => this.getReady(),
237
+ sendRequestOwned: (method, params, options) => {
238
+ const rpc = this.rpc;
239
+ if (!rpc || this._status !== "running") {
240
+ return {
241
+ result: Promise.reject(new Error("client not running")),
242
+ settled: Promise.resolve(),
243
+ };
244
+ }
245
+ return rpc.sendRequestOwned(method, params, options);
246
+ },
247
+ });
248
+ this.diagnosticRequestAdapter = createPriorityDiagnosticRequestAdapter([
249
+ nativeDiagnosticAdapter,
250
+ typescriptDiagnosticAdapter,
251
+ ]);
208
252
  this.diagnostics = new ClientDiagnostics({
209
253
  server: name,
210
254
  cwd: cwd,
211
255
  isOperational: () => this.rpc !== null && this._status === "running",
212
- supportsPullDiagnostics: () => this.hasDiagnosticProvider,
256
+ diagnosticRequestAdapter: this.diagnosticRequestAdapter,
213
257
  usesIncrementalDocumentSync: () => this.usesIncrementalDocumentSync,
214
258
  sendNotification: (method, params) => {
215
259
  if (this.rpc) void this.rpc.sendNotification(method, params);
216
260
  },
217
- pullDocumentDiagnostics: async (request) => {
218
- const rpc = this.rpc;
219
- if (!rpc || this._status !== "running") throw new Error("client not running");
220
- await this.getReady({
221
- signal: request.signal,
222
- deadline: request.deadline,
223
- });
224
- return rpc.sendRequest(
225
- DOCUMENT_DIAGNOSTIC_METHOD,
226
- {
227
- textDocument: { uri: request.uri },
228
- previousResultId: request.previousResultId,
229
- },
230
- {
231
- timeoutMs: request.timeoutMs,
232
- signal: request.signal,
233
- deadline: request.deadline,
234
- operationId: request.operationId,
235
- },
236
- ) as Promise<DocumentDiagnosticReport>;
237
- },
238
261
  });
239
262
  }
240
263
 
@@ -552,18 +575,108 @@ export class LspClient {
552
575
  this.diagnostics.clearPullResultIds();
553
576
  }
554
577
 
555
- /** Check if server supports pull diagnostics. */
578
+ /** Check if the server advertises a valid native pull provider. */
556
579
  get hasDiagnosticProvider(): boolean {
557
- // Static state: a valid `diagnosticProvider` in the initialize result.
558
- // Dynamic state: an active registration for the diagnostic method. A
559
- // malformed static shape or an empty dynamic set fails closed, so an
560
- // unsupported server never gets pull requests.
561
580
  return (
562
581
  isValidDiagnosticOptions(this.capabilities?.diagnosticProvider) ||
563
582
  this.dynamicRegistrations.has(DOCUMENT_DIAGNOSTIC_METHOD)
564
583
  );
565
584
  }
566
585
 
586
+ /** Whether this route has either native pull or the tested TypeScript adapter. */
587
+ get hasDiagnosticRequestAdapter(): boolean {
588
+ return this.hasDiagnosticProvider || this.isSupportedTypeScriptRouteAndCommand();
589
+ }
590
+
591
+ /** Select a native provider that applies to one document. */
592
+ private getDiagnosticProviderOptions(uri: string): Record<string, unknown> | undefined {
593
+ const staticOptions = this.capabilities?.diagnosticProvider;
594
+ const staticRecord = staticOptions as Record<string, unknown> | undefined;
595
+ if (
596
+ staticRecord !== undefined &&
597
+ isValidDiagnosticOptions(staticRecord) &&
598
+ isDocumentSelectorApplicable(staticRecord.documentSelector, uri)
599
+ ) {
600
+ return staticRecord;
601
+ }
602
+ for (const registration of this.dynamicRegistrations.get(DOCUMENT_DIAGNOSTIC_METHOD)) {
603
+ if (isDocumentSelectorApplicable(registration.options.documentSelector, uri)) {
604
+ return registration.options;
605
+ }
606
+ }
607
+ return undefined;
608
+ }
609
+
610
+ private hasApplicableDiagnosticProvider(uri: string): boolean {
611
+ return this.getDiagnosticProviderOptions(uri) !== undefined;
612
+ }
613
+
614
+ private isSupportedTypeScriptRoute(): boolean {
615
+ const command = path.basename(this.config.command).replace(/\.(?:cmd|exe)$/i, "");
616
+ return command === "typescript-language-server";
617
+ }
618
+
619
+ private isSupportedTypeScriptRouteAndCommand(): boolean {
620
+ return (
621
+ this.isSupportedTypeScriptRoute() &&
622
+ Array.isArray(this.capabilities?.executeCommandProvider?.commands) &&
623
+ this.capabilities.executeCommandProvider.commands.includes("typescript.tsserverRequest")
624
+ );
625
+ }
626
+
627
+ private hasTypeScriptRequestCommand(): boolean {
628
+ return this.isSupportedTypeScriptRouteAndCommand();
629
+ }
630
+
631
+ /** Start one native pull request without transferring caller ownership. */
632
+ private startNativeDiagnosticRequest(request: DiagnosticPullRequest): DiagnosticRequestExecution<{
633
+ source: "pull";
634
+ report: DocumentDiagnosticReport;
635
+ }> {
636
+ let activeSettled = Promise.resolve();
637
+ const result = (async () => {
638
+ const ownerDeadline = Number.isFinite(request.timeoutMs)
639
+ ? Date.now() + request.timeoutMs
640
+ : undefined;
641
+ await raceRequestControl(this.getReady(), {
642
+ signal: request.signal,
643
+ deadline: ownerDeadline,
644
+ });
645
+ const rpc = this.rpc;
646
+ if (!rpc || this._status !== "running") throw new Error("client not running");
647
+ const provider = this.getDiagnosticProviderOptions(request.uri);
648
+ if (!provider) throw new Error("No native diagnostic provider applies to this document.");
649
+ const owned = rpc.sendRequestOwned(
650
+ DOCUMENT_DIAGNOSTIC_METHOD,
651
+ {
652
+ textDocument: { uri: request.uri },
653
+ previousResultId: request.previousResultId,
654
+ ...(typeof provider.identifier === "string" ? { identifier: provider.identifier } : {}),
655
+ },
656
+ {
657
+ timeoutMs: request.timeoutMs,
658
+ deadline: ownerDeadline,
659
+ ...(request.operationId !== undefined ? { operationId: request.operationId } : {}),
660
+ },
661
+ );
662
+ activeSettled = owned.settled;
663
+ const report = (await raceRequestControl(owned.result, {
664
+ signal: request.signal,
665
+ deadline: ownerDeadline,
666
+ })) as DocumentDiagnosticReport;
667
+ return { source: "pull" as const, report };
668
+ })();
669
+ const settled = result
670
+ .then(
671
+ () => activeSettled,
672
+ () => activeSettled,
673
+ )
674
+ .then(() => undefined);
675
+ result.catch(() => {});
676
+ settled.catch(() => {});
677
+ return { result, settled };
678
+ }
679
+
567
680
  /** Notify the server that watched workspace files changed. */
568
681
  notifyWorkspaceFileChanges(changes: FileEvent[]): void {
569
682
  if (!this.rpc || this._status !== "running" || changes.length === 0) return;
@@ -580,13 +693,18 @@ export class LspClient {
580
693
  return this.diagnostics.refreshOpenDiagnostics(options);
581
694
  }
582
695
 
583
- /** Sync one file and return diagnostics with explicit evidence availability. */
696
+ /**
697
+ * Sync one file and return diagnostics with explicit evidence availability.
698
+ * The manager disables content authority so this barrier can detect a disk
699
+ * deletion between the manager read and the client request.
700
+ */
584
701
  async syncAndWaitForDiagnostics(
585
702
  filePath: string,
586
703
  content: string,
587
704
  control?: CodeRequestControl,
705
+ options: { contentIsAuthoritative?: boolean } = {},
588
706
  ): Promise<CodeQueryResult<Diagnostic[]>> {
589
- return this.diagnostics.syncAndWaitForDiagnostics(filePath, content, control);
707
+ return this.diagnostics.syncAndWaitForDiagnostics(filePath, content, control, options);
590
708
  }
591
709
 
592
710
  // ── LSP Requests ───────────────────────────────────────────────────
@@ -707,9 +825,7 @@ export class LspClient {
707
825
  );
708
826
  }
709
827
  try {
710
- await this.getReady(control);
711
- const data = (await this.rpc.sendRequest(method, params, control)) as T | null | undefined;
712
- return completedCodeQuery(data ?? null);
828
+ return await this.queryWithEnrollmentRetry(method, params, control);
713
829
  } catch (error) {
714
830
  // Cancellation and absolute-deadline expiry propagate as interruptions:
715
831
  // the caller no longer awaits a result, so no unavailable outcome may
@@ -720,6 +836,125 @@ export class LspClient {
720
836
  }
721
837
  }
722
838
 
839
+ private async queryWithEnrollmentRetry<T>(
840
+ method: string,
841
+ params: unknown,
842
+ control?: CodeRequestControl,
843
+ ): Promise<CodeQueryResult<T | null>> {
844
+ const retryGuard = new SemanticInputRequestRetryGuard((revision) =>
845
+ this.diagnostics.getSemanticInputChangeSince(revision),
846
+ );
847
+ for (;;) {
848
+ const attempt: SemanticRequestAttemptState = { inputRevision: undefined };
849
+ try {
850
+ const data = await this.runSemanticRequestAttempt<T>({
851
+ method,
852
+ params,
853
+ control,
854
+ retryGuard,
855
+ attempt,
856
+ });
857
+ if (retryGuard.hasRetried) {
858
+ this.recordSemanticRetry({ method, control, retryGuard, outcome: "completed" });
859
+ }
860
+ return completedCodeQuery(data);
861
+ } catch (error) {
862
+ this.prepareSemanticRetry({ method, control, retryGuard, attempt, error });
863
+ }
864
+ }
865
+ }
866
+
867
+ /** Return only when another attempt is permitted; retain the cause when retry stops. */
868
+ private prepareSemanticRetry(options: {
869
+ method: string;
870
+ control?: CodeRequestControl;
871
+ retryGuard: SemanticInputRequestRetryGuard;
872
+ attempt: SemanticRequestAttemptState;
873
+ error: unknown;
874
+ }): void {
875
+ const { method, control, retryGuard, attempt, error } = options;
876
+ if (isCodeRequestInterruption(error, control)) {
877
+ if (retryGuard.hasRetried) {
878
+ this.recordSemanticRetry({ method, control, retryGuard, outcome: "interrupted" });
879
+ }
880
+ throwIfCodeRequestInterrupted(control);
881
+ throw error;
882
+ }
883
+ throwIfCodeRequestInterrupted(control);
884
+ const decision = retryGuard.decide(error, attempt.inputRevision);
885
+ if (decision.kind === "retry") {
886
+ throwIfCodeRequestInterrupted(control);
887
+ this.recordSemanticRetry({ method, control, retryGuard, outcome: "retry", error });
888
+ return;
889
+ }
890
+ if (retryGuard.hasRetried) {
891
+ const outcome = isSemanticInputEnrollmentError(decision.error) ? "exhausted" : "failed";
892
+ this.recordSemanticRetry({ method, control, retryGuard, outcome, error: decision.error });
893
+ const detail =
894
+ decision.error instanceof Error ? decision.error.message : String(decision.error);
895
+ const count = retryGuard.retryCount;
896
+ throw new Error(
897
+ `${detail} Enrollment retry ${outcome} after ${count} ${count === 1 ? "retry" : "retries"}.`,
898
+ {
899
+ cause: decision.error,
900
+ },
901
+ );
902
+ }
903
+ throw decision.error;
904
+ }
905
+
906
+ /** Record request acceptance separately from transport completion, without request content. */
907
+ private recordSemanticRetry(options: {
908
+ method: string;
909
+ control?: CodeRequestControl;
910
+ retryGuard: SemanticInputRequestRetryGuard;
911
+ outcome: "retry" | "completed" | "exhausted" | "failed" | "interrupted";
912
+ error?: unknown;
913
+ }): void {
914
+ recordDebugEvent({
915
+ source: "lsp",
916
+ level: "debug",
917
+ category: "semantic-request.enrollment-retry",
918
+ message: "LSP semantic request enrollment retry",
919
+ cwd: boundCwd(this.cwd),
920
+ ...(options.control?.operationId ? { operationId: options.control.operationId } : {}),
921
+ data: {
922
+ server: truncateIdentity(this.name),
923
+ method: truncateIdentity(options.method),
924
+ outcome: options.outcome,
925
+ retryCount: options.retryGuard.retryCount,
926
+ changeKind:
927
+ options.error instanceof SemanticInputSynchronizationError
928
+ ? options.error.changeKind
929
+ : null,
930
+ },
931
+ });
932
+ }
933
+
934
+ private async runSemanticRequestAttempt<T>(options: {
935
+ method: string;
936
+ params: unknown;
937
+ control?: CodeRequestControl;
938
+ retryGuard: SemanticInputRequestRetryGuard;
939
+ attempt: SemanticRequestAttemptState;
940
+ }): Promise<T | null> {
941
+ const { method, params, control, retryGuard, attempt } = options;
942
+ throwIfCodeRequestInterrupted(control);
943
+ retryGuard.assertCurrent();
944
+ await this.getReady(control);
945
+ const inputSnapshot = await this.diagnostics.synchronizeSemanticInputs(control);
946
+ attempt.inputRevision = inputSnapshot.revision;
947
+ throwIfCodeRequestInterrupted(control);
948
+ retryGuard.assertCurrent();
949
+ const rpc = this.rpc;
950
+ if (!rpc || this._status !== "running") {
951
+ throw new Error(`LSP request ${method} is unavailable because the client is not running.`);
952
+ }
953
+ const data = (await rpc.sendRequest(method, params, control)) as T | null | undefined;
954
+ await this.diagnostics.assertSemanticInputsCurrent(inputSnapshot, control);
955
+ return data ?? null;
956
+ }
957
+
723
958
  private async request<T>(
724
959
  method: string,
725
960
  params: unknown,
@@ -758,15 +993,15 @@ export class LspClient {
758
993
  }
759
994
  }
760
995
 
761
- /** Run a server-requested refresh through the forced diagnostic path. */
996
+ /** Run a server-requested refresh through the diagnostic-only path. */
762
997
  private refreshForServerRequest(): Promise<DiagnosticEvidenceSummary> {
763
998
  return this.diagnostics.refreshForServerRequest();
764
999
  }
765
1000
 
766
1001
  private handleServerDiagnosticRefreshRequest(): null {
767
- // Defer the pass before invoking it. Its setup rereads and resynchronizes
768
- // tracked documents, so even an async method can otherwise delay the null
769
- // response on the JSON-RPC request stack.
1002
+ // Defer the pass before invoking it. Its setup can read and synchronize
1003
+ // genuinely changed tracked documents, so even an async method can delay
1004
+ // the null response on the JSON-RPC request stack.
770
1005
  void Promise.resolve()
771
1006
  .then(() => this.refreshForServerRequest())
772
1007
  .then(
@@ -820,6 +1055,10 @@ export class LspClient {
820
1055
  */
821
1056
  private handleRegisterCapability(params: unknown): null {
822
1057
  const registrations = readRegistrations(params, "client/registerCapability");
1058
+ const diagnosticRegistrations: Array<{
1059
+ id: string;
1060
+ options: Record<string, unknown>;
1061
+ }> = [];
823
1062
  for (const registration of registrations) {
824
1063
  if (registration.method !== DOCUMENT_DIAGNOSTIC_METHOD) continue;
825
1064
  if (!isValidDiagnosticOptions(registration.registerOptions)) {
@@ -828,7 +1067,17 @@ export class LspClient {
828
1067
  "Malformed textDocument/diagnostic registration options.",
829
1068
  );
830
1069
  }
831
- this.dynamicRegistrations.register(registration.method, registration.id);
1070
+ diagnosticRegistrations.push({
1071
+ id: registration.id,
1072
+ options: registration.registerOptions as Record<string, unknown>,
1073
+ });
1074
+ }
1075
+ for (const registration of diagnosticRegistrations) {
1076
+ this.dynamicRegistrations.register(
1077
+ DOCUMENT_DIAGNOSTIC_METHOD,
1078
+ registration.id,
1079
+ registration.options,
1080
+ );
832
1081
  }
833
1082
  return null;
834
1083
  }
@@ -844,10 +1093,14 @@ export class LspClient {
844
1093
  if (!isRecord(params) || !Array.isArray(params.unregisterations)) {
845
1094
  throw new JsonRpcRequestError(-32602, "Malformed client/unregisterCapability params.");
846
1095
  }
1096
+ const unregistrations: Array<{ id: string; method: string }> = [];
847
1097
  for (const entry of params.unregisterations) {
848
1098
  if (!isRecord(entry) || typeof entry.id !== "string" || typeof entry.method !== "string") {
849
1099
  throw new JsonRpcRequestError(-32602, "Malformed client/unregisterCapability entry.");
850
1100
  }
1101
+ unregistrations.push({ id: entry.id, method: entry.method });
1102
+ }
1103
+ for (const entry of unregistrations) {
851
1104
  if (entry.method !== DOCUMENT_DIAGNOSTIC_METHOD) continue;
852
1105
  this.dynamicRegistrations.unregister(entry.method, entry.id);
853
1106
  }
@@ -1,6 +1,7 @@
1
1
  // JSON-RPC 2.0 transport — thin wrapper around vscode-jsonrpc.
2
2
  // Handles Content-Length framing, request/response correlation, timeouts,
3
3
  // and notification/request dispatching through vscode-jsonrpc's MessageConnection.
4
+ // biome-ignore-all lint/style/noExcessiveLinesPerFile: Transport lifetime and protocol handling stay together.
4
5
 
5
6
  import type { Readable, Writable } from "node:stream";
6
7
  import {
@@ -103,13 +104,44 @@ export class JsonRpcClient {
103
104
  this.requestHandler = handler;
104
105
  }
105
106
 
107
+ /**
108
+ * Send one request while exposing its underlying transport lifetime.
109
+ * Callers that time out can stop waiting on `result` and keep the route
110
+ * occupied until `settled` resolves.
111
+ */
112
+ sendRequestOwned(
113
+ method: string,
114
+ params?: unknown,
115
+ options?: { timeoutMs?: number } & CodeRequestControl,
116
+ ): { result: Promise<unknown>; settled: Promise<void> } {
117
+ let settle!: () => void;
118
+ const settled = new Promise<void>((resolve) => {
119
+ settle = resolve;
120
+ });
121
+ let result: Promise<unknown>;
122
+ try {
123
+ result = this.sendRequest(method, params, {
124
+ ...options,
125
+ onSettled: settle,
126
+ owned: true,
127
+ });
128
+ } catch (error) {
129
+ settle();
130
+ result = Promise.reject(error);
131
+ }
132
+ result.catch(() => {});
133
+ return { result, settled };
134
+ }
135
+
106
136
  /** Send a request and wait for the correlated response, optionally overriding the timeout. */
137
+ // biome-ignore lint/complexity/noExcessiveLinesPerFunction: Request timeout and cancellation races stay together.
107
138
  sendRequest(
108
139
  method: string,
109
140
  params?: unknown,
110
- options?: { timeoutMs?: number } & CodeRequestControl,
141
+ options?: { timeoutMs?: number; onSettled?: () => void; owned?: boolean } & CodeRequestControl,
111
142
  ): Promise<unknown> {
112
143
  const timeoutMs = options?.timeoutMs ?? this.timeoutMs;
144
+ const owned = options?.owned === true;
113
145
  const signal = options?.signal;
114
146
  const deadlineMs = options?.deadline === undefined ? undefined : options.deadline - Date.now();
115
147
  const methodClass = classifyRequestMethod(method);
@@ -117,7 +149,7 @@ export class JsonRpcClient {
117
149
  const timer = startDebugTimer();
118
150
  // A request cancelled before dispatch must not produce protocol traffic.
119
151
  if (signal?.aborted) {
120
- return rejectUndispatchedRequest(
152
+ const result = rejectUndispatchedRequest(
121
153
  signal.reason ?? new Error(`Request ${method} was cancelled`),
122
154
  {
123
155
  timer,
@@ -126,11 +158,13 @@ export class JsonRpcClient {
126
158
  identity: { server: this.server, cwd: this.cwd },
127
159
  },
128
160
  );
161
+ options?.onSettled?.();
162
+ return result;
129
163
  }
130
164
  // An expired absolute deadline must not even send the request: the caller
131
165
  // no longer awaits a result, so no protocol traffic may start.
132
166
  if (deadlineMs !== undefined && deadlineMs <= 0) {
133
- return rejectUndispatchedRequest(new CodeRequestDeadlineError(), {
167
+ const result = rejectUndispatchedRequest(new CodeRequestDeadlineError(), {
134
168
  timer,
135
169
  operationId: options?.operationId,
136
170
  observation: {
@@ -141,18 +175,24 @@ export class JsonRpcClient {
141
175
  },
142
176
  identity: { server: this.server, cwd: this.cwd },
143
177
  });
178
+ options?.onSettled?.();
179
+ return result;
144
180
  }
145
181
  if (this.closed || !this.connection) {
146
- return rejectUndispatchedRequest(new Error("JSON-RPC client is closed"), {
182
+ const result = rejectUndispatchedRequest(new Error("JSON-RPC client is closed"), {
147
183
  timer,
148
184
  operationId: options?.operationId,
149
185
  observation: { method: boundedMethod, methodClass, outcome: "cancelled" },
150
186
  identity: { server: this.server, cwd: this.cwd },
151
187
  });
188
+ options?.onSettled?.();
189
+ return result;
152
190
  }
153
191
  const tokenSource = new CancellationTokenSource();
154
192
  let timeout: ReturnType<typeof setTimeout> | undefined;
193
+ let transportTimeout: ReturnType<typeof setTimeout> | undefined;
155
194
  let timedOut = false;
195
+ let transportTimedOut = false;
156
196
  let aborted = false;
157
197
  const timerDelayMs = deadlineMs === undefined ? timeoutMs : Math.min(timeoutMs, deadlineMs);
158
198
  const timeoutError = Object.assign(
@@ -165,16 +205,34 @@ export class JsonRpcClient {
165
205
  // Catch the raw request promise to prevent unhandled rejections when
166
206
  // dispose() cancels the token without a preceding timeout.
167
207
  request.catch(() => {});
208
+ request.then(
209
+ () => {
210
+ if (transportTimeout !== undefined) clearTimeout(transportTimeout);
211
+ options?.onSettled?.();
212
+ },
213
+ () => {
214
+ if (transportTimeout !== undefined) clearTimeout(transportTimeout);
215
+ options?.onSettled?.();
216
+ },
217
+ );
218
+ if (owned) {
219
+ const transportTimeoutMs = Number.isFinite(timeoutMs)
220
+ ? Math.max(this.timeoutMs, timeoutMs)
221
+ : this.timeoutMs;
222
+ transportTimeout = setTimeout(() => {
223
+ transportTimedOut = true;
224
+ tokenSource.cancel();
225
+ }, transportTimeoutMs);
226
+ }
168
227
 
169
- // Race the request against a single shared timeout that both cancels
170
- // the JSON-RPC token and rejects the caller. Using one timer avoids
171
- // a leak where the rejecting timer in a second Promise stays alive
172
- // after a successful response.
228
+ // Race the request against the caller's timeout. Ordinary requests cancel
229
+ // the JSON-RPC token; owned requests leave it active so the route stays
230
+ // occupied until the raw request settles.
173
231
  let abortHandler: (() => void) | undefined;
174
232
  const abort = new Promise<never>((_resolve, reject) => {
175
233
  abortHandler = () => {
176
234
  aborted = true;
177
- tokenSource.cancel();
235
+ if (!owned) tokenSource.cancel();
178
236
  // Reject with the caller's abort reason when one exists, matching the
179
237
  // canonical throwIfCodeRequestInterrupted() behavior.
180
238
  reject(signal?.reason ?? abortError);
@@ -187,7 +245,7 @@ export class JsonRpcClient {
187
245
  new Promise<never>((_resolve, reject) => {
188
246
  timeout = setTimeout(() => {
189
247
  timedOut = true;
190
- tokenSource.cancel();
248
+ if (!owned) tokenSource.cancel();
191
249
  // A deadline that binds earlier than the timeout is a deadline
192
250
  // outcome, distinct from an ordinary per-request timeout.
193
251
  reject(
@@ -214,12 +272,10 @@ export class JsonRpcClient {
214
272
  return result;
215
273
  },
216
274
  (error: unknown) => {
217
- const cancelled = timedOut || aborted || this.closed || isCancellationError(error);
218
- const outcome: RequestOutcome = timedOut
219
- ? "timed-out"
220
- : cancelled
221
- ? "cancelled"
222
- : "failed";
275
+ const cancelled =
276
+ timedOut || transportTimedOut || aborted || this.closed || isCancellationError(error);
277
+ const outcome: RequestOutcome =
278
+ timedOut || transportTimedOut ? "timed-out" : cancelled ? "cancelled" : "failed";
223
279
  const errorCode = requestErrorCode(outcome, error);
224
280
  this.countProtocolStallFailure(outcome, errorCode, error);
225
281
  recordRequestTiming(
@@ -49,6 +49,7 @@ export const CLIENT_CAPABILITIES: ClientCapabilities = {
49
49
  },
50
50
  publishDiagnostics: {
51
51
  relatedInformation: true,
52
+ tagSupport: { valueSet: [1, 2] },
52
53
  versionSupport: true,
53
54
  },
54
55
  diagnostic: {
@@ -1,9 +1,9 @@
1
1
  // tsconfig-aware file scope detection.
2
2
  //
3
- // Determines whether a file is within the compilation scope of its nearest
4
- // tsconfig.json or jsconfig.json using the TypeScript compiler's own config
5
- // parsing APIs. Used by the diagnostic filter to suppress LSP errors on files
6
- // that TypeScript itself would not include in the project.
3
+ // Determines whether a TypeScript or JavaScript-family file is within the
4
+ // compilation scope of its nearest tsconfig.json or jsconfig.json using the
5
+ // TypeScript compiler's own config parsing APIs. Used by the diagnostic filter
6
+ // to suppress LSP errors on files that TypeScript itself would not include.
7
7
 
8
8
  import * as path from "node:path";
9
9
  import ts from "typescript";
@@ -54,6 +54,23 @@ export interface FileScopeDecision {
54
54
  caseSensitiveFileNames: boolean;
55
55
  }
56
56
 
57
+ const TYPESCRIPT_JAVASCRIPT_EXTENSIONS = new Set([
58
+ ".ts",
59
+ ".tsx",
60
+ ".mts",
61
+ ".cts",
62
+ ".js",
63
+ ".jsx",
64
+ ".mjs",
65
+ ".cjs",
66
+ ]);
67
+
68
+ /** Whether tsconfig/jsconfig filtering applies to a TypeScript or JavaScript-family file. */
69
+ export function isTsconfigApplicableFile(filePath: string): boolean {
70
+ const normalizedPath = filePath.replaceAll("\\", "/");
71
+ return TYPESCRIPT_JAVASCRIPT_EXTENSIONS.has(path.extname(normalizedPath).toLowerCase());
72
+ }
73
+
57
74
  const nearestConfigCache = new Map<string, string | null>();
58
75
  const parsedConfigCache = new Map<string, ParsedProjectConfigCacheEntry>();
59
76
 
@@ -82,6 +99,10 @@ const tsInternal = ts as typeof ts & {
82
99
  * @returns `true` if the file is excluded from compilation scope
83
100
  */
84
101
  export function isFileExcludedByTsconfig(filePath: string, cwd: string): boolean {
102
+ // TypeScript project scope does not decide whether another language's LSP
103
+ // diagnostics belong in the automatic workspace summary.
104
+ if (!isTsconfigApplicableFile(filePath)) return false;
105
+
85
106
  // Legacy filter semantics: only a resolved "excluded" decision filters a
86
107
  // file. Out-of-tree and no-config files stay unfiltered; the decision API
87
108
  // still reports those statuses honestly for consumers that want them.
@@ -91,6 +112,11 @@ export function isFileExcludedByTsconfig(filePath: string, cwd: string): boolean
91
112
  /**
92
113
  * Compute the explainable tsconfig scope decision for one file.
93
114
  *
115
+ * This low-level TypeScript compilation-membership query returns an
116
+ * extension-based decision for other languages. Diagnostic filtering and
117
+ * display first use {@link isTsconfigApplicableFile}; language-neutral
118
+ * consumers should use that helper or {@link isFileExcludedByTsconfig} instead.
119
+ *
94
120
  * The decision carries the mechanism that produced it (the scope decision
95
121
  * basis) so consumers can report why a file is inside or outside the nearest
96
122
  * project config without re-deriving the scope logic.
@@ -1,6 +1,6 @@
1
1
  /** Reason for a partial or unavailable single-file result when the push stays tentative. */
2
2
  export const TENTATIVE_PUSH_UNAVAILABLE_REASON =
3
- "The current push publication is tentative: a later diagnostic republish for the same document synchronization is required.";
3
+ "The current diagnostic publication is ambient evidence and cannot confirm the document synchronization.";
4
4
 
5
5
  /** A final evidence state for one tracked document. */
6
6
  export type DiagnosticEvidenceStatus = "confirmed" | "unconfirmed" | "failed" | "removed";