@mrclrchtr/supi-code-intelligence 4.9.0 → 5.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 (215) hide show
  1. package/README.md +2 -1
  2. package/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  3. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  5. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  6. package/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  7. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  8. package/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  9. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  10. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  13. package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  14. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  15. package/node_modules/@mrclrchtr/supi-lsp/README.md +80 -5
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  23. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  24. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  26. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  27. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  28. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  29. package/node_modules/@mrclrchtr/supi-lsp/package.json +7 -6
  30. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +10 -0
  31. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +206 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +78 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +114 -0
  34. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +372 -0
  35. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +14 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +62 -0
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +491 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +9 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +215 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +206 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +370 -266
  42. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +52 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +185 -0
  44. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-file-state.ts +25 -0
  45. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +467 -115
  46. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +238 -11
  47. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +10 -1
  48. package/node_modules/@mrclrchtr/supi-lsp/src/config/defaults.json +1 -1
  49. package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +45 -0
  50. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +38 -0
  51. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +79 -9
  52. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +13 -4
  53. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +300 -24
  54. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +5 -2
  55. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +464 -96
  56. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-refactor-provider.ts +130 -0
  57. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +45 -329
  58. package/node_modules/@mrclrchtr/supi-lsp/src/provider/refactor-planning.ts +57 -120
  59. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-edit-normalizer.ts +320 -0
  60. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +207 -0
  61. package/node_modules/@mrclrchtr/supi-lsp/src/session/readiness.ts +72 -2
  62. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +151 -26
  63. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +40 -0
  64. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +50 -0
  65. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +136 -120
  66. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +49 -0
  67. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +102 -0
  68. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +26 -6
  69. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  70. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  71. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  72. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  73. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  74. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  75. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/LICENSE +21 -0
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/README.md +258 -0
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/babel.cjs +257 -0
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/jiti.cjs +1 -0
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-cli.mjs +34 -0
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-hooks.mjs +124 -0
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-native.mjs +121 -0
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.d.mts +1 -0
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.mjs +4 -0
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-static.mjs +23 -0
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.cjs +30 -0
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.cts +8 -0
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.mts +8 -0
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.mjs +29 -0
  96. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/types.d.ts +420 -0
  97. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/package.json +146 -0
  98. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
  99. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +4 -4
  100. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +1 -1
  101. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
  102. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +4 -4
  103. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +7 -3
  104. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
  105. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
  106. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +0 -9
  107. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -8
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +16 -12
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +65 -87
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +47 -95
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-timing.ts +147 -0
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client-lifecycle.ts +39 -0
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +561 -0
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-message-size.ts +24 -0
  115. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +452 -0
  116. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +4 -2
  117. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +12 -4
  118. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +4 -2
  119. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/imports.ts +4 -2
  120. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +5 -3
  121. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +33 -12
  122. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/blocking-test-bootstrap.mjs +34 -0
  123. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/bootstrap.mjs +8 -0
  124. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store-helpers.ts +23 -0
  125. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +400 -0
  126. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/request-control.ts +48 -0
  127. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-parser-helpers.ts +24 -0
  128. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-query-helpers.ts +43 -0
  129. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +421 -0
  130. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +116 -0
  131. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +201 -0
  132. package/package.json +5 -5
  133. package/src/analysis/health/diagnostics.ts +236 -24
  134. package/src/analysis/health/recovery.ts +6 -2
  135. package/src/analysis/provider.ts +58 -0
  136. package/src/analysis/readiness.ts +30 -9
  137. package/src/analysis/refactor/apply.ts +59 -20
  138. package/src/analysis/refactor/mutation-authority.ts +143 -0
  139. package/src/analysis/refactor/position.ts +47 -0
  140. package/src/analysis/refactor/safety.ts +18 -10
  141. package/src/analysis/relations/callees.ts +9 -1
  142. package/src/analysis/relations/types.ts +5 -2
  143. package/src/analysis/search/ast-scan-timing.ts +9 -4
  144. package/src/analysis/search/ast-scan.ts +1 -1
  145. package/src/analysis/search/deadline.ts +2 -2
  146. package/src/analysis/search/pattern-analysis.ts +46 -32
  147. package/src/analysis/search/pattern.ts +12 -2
  148. package/src/analysis/target/anchored.ts +20 -11
  149. package/src/analysis/target/file.ts +18 -11
  150. package/src/analysis/target/identity.ts +19 -3
  151. package/src/analysis/target/symbol.ts +36 -15
  152. package/src/api.ts +4 -0
  153. package/src/diagnostics/evidence.ts +51 -0
  154. package/src/session/capability-adapter.ts +12 -3
  155. package/src/session/find-workflow.ts +17 -7
  156. package/src/session/graph/collect.ts +18 -2
  157. package/src/session/graph-workflow.ts +19 -12
  158. package/src/session/health-refresh.ts +148 -0
  159. package/src/session/health-types.ts +37 -15
  160. package/src/session/health-workflow.ts +37 -39
  161. package/src/session/inspect/collect.ts +20 -3
  162. package/src/session/inspect-workflow.ts +10 -5
  163. package/src/session/orientation/collect.ts +10 -1
  164. package/src/session/orientation/context-facts.ts +3 -1
  165. package/src/session/orientation/context-sections.ts +20 -7
  166. package/src/session/orientation/gather.ts +17 -11
  167. package/src/session/orientation-types.ts +2 -1
  168. package/src/session/orientation-workflow.ts +15 -8
  169. package/src/session/refactor-plans.ts +2 -0
  170. package/src/session/refactor-workflow.ts +59 -14
  171. package/src/session/session.ts +4 -1
  172. package/src/session/target-workflow.ts +54 -23
  173. package/src/session/workflow-control.ts +7 -3
  174. package/src/substrate/lsp/lifecycle.ts +26 -46
  175. package/src/substrate/lsp/maintenance.ts +199 -25
  176. package/src/substrate/lsp/state.ts +2 -2
  177. package/src/substrate/workspace-provider-host.ts +25 -3
  178. package/src/tool/find/execute.ts +7 -1
  179. package/src/tool/find/render.ts +20 -3
  180. package/src/tool/find/tui.ts +23 -21
  181. package/src/tool/graph/execute.ts +23 -3
  182. package/src/tool/graph/tui.ts +59 -46
  183. package/src/tool/health/execute.ts +7 -2
  184. package/src/tool/health/markdown.ts +56 -14
  185. package/src/tool/health/refresh-status.ts +105 -0
  186. package/src/tool/health/tui.ts +95 -98
  187. package/src/tool/infra/workflow-control.ts +8 -0
  188. package/src/tool/inspect/execute.ts +7 -1
  189. package/src/tool/inspect/tui.ts +26 -18
  190. package/src/tool/orientation/execute.ts +21 -3
  191. package/src/tool/orientation/tui.ts +51 -35
  192. package/src/tool/refactor-apply/execute.ts +3 -0
  193. package/src/tool/refactor-apply/tui.ts +43 -25
  194. package/src/tool/refactor-plan/execute.ts +4 -0
  195. package/src/tool/refactor-plan/tui.ts +33 -20
  196. package/src/tool/register.ts +127 -22
  197. package/src/tool/resolve/execute.ts +18 -1
  198. package/src/tool/resolve/tui.ts +42 -24
  199. package/src/tool/result/display.ts +107 -0
  200. package/src/tool/result/errors.ts +20 -17
  201. package/src/tool/result/find.ts +52 -8
  202. package/src/tool/result/graph.ts +44 -1
  203. package/src/tool/result/health.ts +53 -1
  204. package/src/tool/result/inspect.ts +113 -1
  205. package/src/tool/result/orientation.ts +66 -14
  206. package/src/tool/result/refactor.ts +36 -4
  207. package/src/tool/result/resolve.ts +44 -7
  208. package/src/tool/result/types.ts +20 -0
  209. package/src/types/execution.ts +18 -5
  210. package/src/types/index.ts +8 -1
  211. package/src/ui/status-command.ts +5 -3
  212. package/src/ui/status-overlay.ts +4 -0
  213. package/src/ui/tui/common.ts +108 -10
  214. package/src/ui/tui/display.ts +48 -0
  215. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +0 -247
@@ -2,9 +2,23 @@
2
2
  // biome-ignore-all lint/style/noExcessiveLinesPerFile: LspManager stays cohesive; recovery and sync helpers are split into manager-*.ts modules.
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
- import { type CodeQueryResult, unavailableCodeQuery } from "@mrclrchtr/supi-code-runtime/api";
5
+ import {
6
+ type CodeQueryResult,
7
+ type CodeRequestControl,
8
+ isCodeRequestInterruption,
9
+ throwIfCodeRequestInterrupted,
10
+ unavailableCodeQuery,
11
+ } from "@mrclrchtr/supi-code-runtime/api";
6
12
  import * as projectRoots from "@mrclrchtr/supi-core/project";
7
- import { LspClient } from "../client/client.ts";
13
+ import {
14
+ LspClient,
15
+ type LspClientLifecycleTransitionKind,
16
+ RECOVERY_CLIENT_STARTUP_BOUND_MS,
17
+ withTimeout,
18
+ } from "../client/client.ts";
19
+
20
+ export { RECOVERY_CLIENT_STARTUP_BOUND_MS } from "../client/client.ts";
21
+
8
22
  import { getServerForFile } from "../config/config.ts";
9
23
  import type {
10
24
  DetectedProjectServer,
@@ -22,6 +36,12 @@ import {
22
36
  createOutstandingDiagnosticSummary,
23
37
  relativeFilePathFromUri,
24
38
  } from "../diagnostics/diagnostic-summary.ts";
39
+ import {
40
+ type DiagnosticEvidenceDocument,
41
+ type DiagnosticEvidenceSummary,
42
+ summarizeDiagnosticEvidence,
43
+ } from "../diagnostics/evidence.ts";
44
+ import { raceRequestControl } from "../session/readiness.ts";
25
45
  import {
26
46
  displayRelativeFilePath,
27
47
  formatCoverageSummaryText,
@@ -30,7 +50,7 @@ import {
30
50
  normalizeRelevantPaths,
31
51
  shouldIgnoreLspPath,
32
52
  } from "../summary.ts";
33
- import { commandExists, resolveSessionPath } from "../utils.ts";
53
+ import { commandExists, resolveSessionPath, uriToFile } from "../utils.ts";
34
54
  import {
35
55
  closeFileAcrossClients,
36
56
  pruneMissingFilesFromClients,
@@ -55,7 +75,10 @@ import type {
55
75
  OutstandingDiagnosticSummaryEntry,
56
76
  ServerStatus,
57
77
  } from "./manager-types.ts";
58
- import { recoverWorkspaceDiagnostics as recoverWorkspaceDiagnosticsImpl } from "./manager-workspace-recovery.ts";
78
+ import {
79
+ recoverWorkspaceDiagnostics as recoverWorkspaceDiagnosticsImpl,
80
+ type WorkspaceRecoveryResult,
81
+ } from "./manager-workspace-recovery.ts";
59
82
  import {
60
83
  collectWorkspaceSymbols,
61
84
  findWorkspaceSymbolWarmTargets,
@@ -73,6 +96,24 @@ type FileRoute = {
73
96
  key: string;
74
97
  };
75
98
 
99
+ type ClientRestartResult = {
100
+ key: string;
101
+ serverName: string;
102
+ files: string[];
103
+ restarted: boolean;
104
+ };
105
+
106
+ export type ManagerLifecycleTransitionKind = LspClientLifecycleTransitionKind | "recovery";
107
+
108
+ /** Package-internal aggregate lifecycle snapshot for the runtime controller. */
109
+ export interface ManagerLifecycleTransition {
110
+ readonly kind: ManagerLifecycleTransitionKind;
111
+ readonly semanticReady: boolean;
112
+ readonly projectServers: readonly ProjectServerInfo[];
113
+ }
114
+
115
+ type ManagerLifecycleListener = (transition: ManagerLifecycleTransition) => void;
116
+
76
117
  // ── LspManager ────────────────────────────────────────────────────────
77
118
  export class LspManager {
78
119
  /** Active clients keyed by "serverName:root" */
@@ -83,10 +124,16 @@ export class LspManager {
83
124
  private commandAvailability = new Map<string, boolean>();
84
125
  /** Guards against concurrent client creation for the same server:root key */
85
126
  private pendingStarts = new Map<string, Promise<LspClient | null>>();
127
+ /** Monotonic ownership generation for each replaceable client route. */
128
+ private clientGenerations = new Map<string, number>();
86
129
  /** Preferred project roots discovered by proactive scan or lazy startup */
87
130
  private knownRoots = new Map<string, string[]>();
88
131
  /** User-configured gitignore-style exclude patterns */
89
132
  private excludePatterns: string[] = [];
133
+ /** Monotonic workspace invalidation generation advanced by each invalidation event. */
134
+ private invalidationEpoch = 0;
135
+ /** Invalidation generation in which each route last received a recovery restart. */
136
+ private recoveryRestartEpochs = new Map<string, number>();
90
137
  /** Project roots already warmed for workspace-symbol queries. */
91
138
  private warmedWorkspaceSymbolProjects = new Set<string>();
92
139
  /** Project roots whose semantic state was warmed with a readiness probe. */
@@ -96,6 +143,7 @@ export class LspManager {
96
143
  constructor(
97
144
  private readonly config: LspConfig,
98
145
  private readonly cwd: string,
146
+ private readonly onLifecycleTransition?: ManagerLifecycleListener,
99
147
  ) {}
100
148
  getCwd(): string {
101
149
  return this.cwd;
@@ -152,6 +200,15 @@ export class LspManager {
152
200
 
153
201
  return reason;
154
202
  }
203
+
204
+ /** Return the open document version without starting a server. */
205
+ getOpenDocumentVersion(filePath: string): number | null {
206
+ const resolvedPath = resolveSessionPath(this.cwd, filePath);
207
+ return (
208
+ this.getExistingClientForFile(resolvedPath)?.getOpenDocumentVersion(resolvedPath) ?? null
209
+ );
210
+ }
211
+
155
212
  /** Get or create an LSP client for the given file. */
156
213
  async getClientForFile(filePath: string): Promise<LspClient | null> {
157
214
  const route = this.resolveFileRoute(filePath);
@@ -194,6 +251,53 @@ export class LspManager {
194
251
  }
195
252
  }
196
253
 
254
+ private createClient(
255
+ serverName: string,
256
+ serverConfig: ServerConfig,
257
+ root: string,
258
+ key: string,
259
+ ): LspClient {
260
+ const generation = (this.clientGenerations.get(key) ?? 0) + 1;
261
+ this.clientGenerations.set(key, generation);
262
+ let client: LspClient;
263
+ client = new LspClient(
264
+ serverName,
265
+ serverConfig,
266
+ root,
267
+ (kind) => {
268
+ this.handleClientLifecycle(key, generation, client, kind);
269
+ },
270
+ this.cwd,
271
+ );
272
+ return client;
273
+ }
274
+
275
+ private handleClientLifecycle(
276
+ key: string,
277
+ generation: number,
278
+ client: LspClient,
279
+ kind: LspClientLifecycleTransitionKind,
280
+ ): void {
281
+ if (this.clientGenerations.get(key) !== generation) return;
282
+ if (this.clients.get(key) !== client) return;
283
+ const aggregateKind =
284
+ kind === "readiness" && generation > 1 && client.ready ? "recovery" : kind;
285
+ this.publishLifecycle(aggregateKind);
286
+ }
287
+
288
+ private publishLifecycle(kind: ManagerLifecycleTransitionKind): void {
289
+ if (!this.onLifecycleTransition) return;
290
+ const projectServers = this.getKnownProjectServers([]);
291
+ const semanticReady = projectServers.some(
292
+ (server) => server.status === "running" && server.ready,
293
+ );
294
+ try {
295
+ this.onLifecycleTransition({ kind, semanticReady, projectServers });
296
+ } catch {
297
+ // Runtime lifecycle consumers must not alter manager behavior.
298
+ }
299
+ }
300
+
197
301
  /**
198
302
  * Perform the actual server start — extracted so the public method can
199
303
  * deduplicate via pendingStarts without wrapping the entire body.
@@ -211,7 +315,7 @@ export class LspManager {
211
315
  }
212
316
 
213
317
  // Spawn new client
214
- const client = new LspClient(serverName, serverConfig, root);
318
+ const client = this.createClient(serverName, serverConfig, root, key);
215
319
  this.clearWarmedWorkspaceSymbolProjects(serverName, root);
216
320
  this.clearWarmedSemanticProjects(serverName, root);
217
321
  this.clearPendingWarmProbes(serverName, root);
@@ -247,33 +351,51 @@ export class LspManager {
247
351
  return route ? (this.clients.get(route.key) ?? null) : null;
248
352
  }
249
353
 
250
- /** Restart the clients that own the supplied file paths, if any are active. */
251
- async restartClientsForFiles(filePaths: string[]): Promise<string[]> {
252
- const restarted: string[] = [];
354
+ /**
355
+ * Restart the clients that own the supplied file paths, if any are active.
356
+ *
357
+ * Each route restarts at most once per invalidation generation. Pass
358
+ * `pushOnly` to restrict restarts to clients without pull diagnostics.
359
+ * The loop observes request cancellation between route restarts.
360
+ */
361
+ async restartClientsForFiles(
362
+ filePaths: string[],
363
+ options?: { pushOnly?: boolean; control?: CodeRequestControl },
364
+ ): Promise<ClientRestartResult[]> {
365
+ const restarted: ClientRestartResult[] = [];
253
366
  const seen = new Set<string>();
254
367
 
255
368
  for (const filePath of filePaths) {
256
369
  const client = this.getExistingClientForFile(filePath);
257
370
  if (!client) continue;
371
+ if (options?.pushOnly && client.hasDiagnosticProvider) continue;
258
372
 
259
373
  const key = clientKey(client.name, client.root);
260
374
  if (seen.has(key)) continue;
261
375
  seen.add(key);
376
+ if (this.recoveryRestartEpochs.get(key) === this.invalidationEpoch) continue;
262
377
 
263
- if (await this.restartClient(client)) {
264
- restarted.push(key);
265
- }
378
+ // Observe cancellation between route restarts so a cancelled pass does
379
+ // not keep restarting further clients.
380
+ throwIfCodeRequestInterrupted(options?.control);
381
+ const result = await this.restartClient(client);
382
+ this.recoveryRestartEpochs.set(key, this.invalidationEpoch);
383
+ restarted.push({ key, serverName: client.name, ...result });
266
384
  }
267
385
 
268
386
  return restarted;
269
387
  }
270
388
 
271
- private async restartClient(client: LspClient): Promise<boolean> {
389
+ private async restartClient(client: LspClient): Promise<{ files: string[]; restarted: boolean }> {
272
390
  const key = clientKey(client.name, client.root);
273
391
  const serverConfig = this.config.servers[client.name];
274
- if (!serverConfig) return false;
392
+ if (!serverConfig) return { files: [], restarted: false };
275
393
 
276
394
  const openFiles = client.openFiles;
395
+ const trackedDiagnosticFiles = client
396
+ .getDiagnosticSnapshot()
397
+ .documents.map((document) => uriToFile(document.uri));
398
+ const files = [...new Set([...openFiles, ...trackedDiagnosticFiles])];
277
399
  try {
278
400
  await client.shutdown();
279
401
  } catch {
@@ -286,25 +408,37 @@ export class LspManager {
286
408
  this.clearWarmedSemanticProjects(client.name, client.root);
287
409
  this.clearPendingWarmProbes(client.name, client.root);
288
410
 
289
- const replacement = new LspClient(client.name, serverConfig, client.root);
411
+ const replacement = this.createClient(client.name, serverConfig, client.root, key);
290
412
  this.clients.set(key, replacement);
291
413
  rememberKnownRoot(this.knownRoots, client.name, client.root);
292
414
 
293
415
  try {
294
- await replacement.start();
295
- for (const filePath of openFiles) {
296
- if (!fs.existsSync(filePath)) continue;
416
+ await withTimeout(
417
+ replacement.start(),
418
+ RECOVERY_CLIENT_STARTUP_BOUND_MS,
419
+ "replacement client startup bound exceeded",
420
+ );
421
+ const failedFiles: string[] = [];
422
+ for (const filePath of files) {
423
+ if (!fs.existsSync(filePath)) {
424
+ failedFiles.push(filePath);
425
+ continue;
426
+ }
297
427
  try {
298
428
  replacement.didOpen(filePath, fs.readFileSync(filePath, "utf-8"));
299
429
  } catch {
300
- // Skip unreadable files on restart.
430
+ failedFiles.push(filePath);
301
431
  }
302
432
  }
303
- return true;
433
+ for (const filePath of failedFiles) replacement.markFailedFile(filePath);
434
+ return { files, restarted: true };
304
435
  } catch {
305
436
  this.clients.delete(key);
306
437
  this.unavailable.set(key, "start-failed");
307
- return false;
438
+ // A bound-exceeded start may have spawned a live process; terminate
439
+ // the tree so the orphan cannot outlive the failed restart.
440
+ void replacement.forceKill().catch(() => {});
441
+ return { files, restarted: false };
308
442
  }
309
443
  }
310
444
 
@@ -347,41 +481,68 @@ export class LspManager {
347
481
  }
348
482
 
349
483
  /** Wait until the client that owns a file is query-ready, then run a light warm-up probe. */
350
- async waitUntilFileReady(filePath: string): Promise<LspClient | null> {
484
+ async waitUntilFileReady(
485
+ filePath: string,
486
+ control?: CodeRequestControl,
487
+ ): Promise<LspClient | null> {
488
+ throwIfCodeRequestInterrupted(control);
351
489
  const resolvedPath = resolveSessionPath(this.cwd, filePath);
352
490
  const client = await this.getClientForFile(resolvedPath);
353
491
  if (!client) return null;
354
- await client.getReady();
355
- await this.warmSemanticProject(client, resolvedPath, true);
492
+ // The caller's wait stops on cancellation even though the shared
493
+ // readiness state keeps its own lifecycle.
494
+ await client.getReady(control);
495
+ await this.warmSemanticProject(client, resolvedPath, true, control);
496
+ throwIfCodeRequestInterrupted(control);
356
497
  return client;
357
498
  }
358
499
 
359
500
  /**
360
- * Wait until all started clients are query-ready, then warm one project file per client/root.
361
- * Returns the number of concrete clients that reached readiness.
501
+ * Wait until one started client is query-ready, then warm its project.
502
+ * Returns the number of concrete clients that are ready after the warm-up.
362
503
  */
363
- async waitUntilWorkspaceReady(): Promise<number> {
504
+ async waitUntilWorkspaceReady(control?: CodeRequestControl): Promise<number> {
505
+ throwIfCodeRequestInterrupted(control);
364
506
  const activeClients = Array.from(this.clients.values()).filter(
365
507
  (client) => client.status === "running",
366
508
  );
367
- await Promise.all(activeClients.map((client) => client.getReady()));
509
+ if (activeClients.length === 0) return 0;
368
510
 
369
- for (const client of activeClients) {
370
- const serverConfig = this.config.servers[client.name];
371
- if (!serverConfig) continue;
511
+ const alreadyReady = activeClients.find((client) => client.ready);
512
+ const firstReady =
513
+ alreadyReady ??
514
+ (await Promise.any(
515
+ activeClients.map(async (client) => {
516
+ await client.getReady(control);
517
+ if (client.status !== "running" || !client.ready) {
518
+ throw new Error("LSP client did not reach concrete readiness.");
519
+ }
520
+ return client;
521
+ }),
522
+ ).catch(() => null));
523
+ // Promise.any swallows individual rejections, so an interruption that
524
+ // raced the readiness wait must be re-raised before it degrades into a
525
+ // zero-ready outcome.
526
+ throwIfCodeRequestInterrupted(control);
527
+ if (!firstReady) return 0;
528
+
529
+ const serverConfig = this.config.servers[firstReady.name];
530
+ if (serverConfig) {
372
531
  const target = findWorkspaceSymbolWarmTargets(
373
- client.root,
532
+ firstReady.root,
374
533
  serverConfig.rootMarkers,
375
534
  serverConfig.fileTypes,
376
535
  )[0];
377
- if (!target) continue;
378
- await this.warmSemanticProject(client, target.file);
536
+ if (target) await this.warmSemanticProject(firstReady, target.file, false, control);
379
537
  }
380
- return activeClients.length;
538
+
539
+ throwIfCodeRequestInterrupted(control);
540
+ return activeClients.filter((client) => client.status === "running" && client.ready).length;
381
541
  }
382
542
  async syncFileAndGetDiagnostics(
383
543
  filePath: string,
384
544
  maxSeverity: number = 1,
545
+ control?: CodeRequestControl,
385
546
  ): Promise<CodeQueryResult<Diagnostic[]>> {
386
547
  const resolvedPath = resolveSessionPath(this.cwd, filePath);
387
548
  const client = await this.getClientForFile(resolvedPath);
@@ -389,11 +550,11 @@ export class LspManager {
389
550
  return unavailableCodeQuery(`No LSP client can collect diagnostics for ${resolvedPath}.`);
390
551
  }
391
552
  try {
392
- return {
393
- kind: "completed",
394
- data: await syncClientFileAndGetDiagnostics(client, resolvedPath, maxSeverity),
395
- };
553
+ return await syncClientFileAndGetDiagnostics(client, resolvedPath, maxSeverity, control);
396
554
  } catch (error) {
555
+ // Cancellation must propagate; a cancelled caller no longer awaits a
556
+ // diagnostic result or the file cleanup side effects.
557
+ if (isCodeRequestInterruption(error, control)) throw error;
397
558
  this.closeFile(resolvedPath);
398
559
  const detail = error instanceof Error ? error.message : String(error);
399
560
  return unavailableCodeQuery(`Diagnostic collection failed for ${resolvedPath}: ${detail}`);
@@ -407,9 +568,12 @@ export class LspManager {
407
568
  pruneMissingFiles(): string[] {
408
569
  return pruneMissingFilesFromClients(this.clients.values());
409
570
  }
410
- /** Re-sync all open documents across active clients and wait for diagnostics to settle. */
411
- async refreshOpenDiagnostics(options?: { maxWaitMs?: number; quietMs?: number }): Promise<void> {
412
- await refreshOpenDiagnosticsForClients(this.clients.values(), options);
571
+ /** Re-sync all open documents across active clients and return exact evidence coverage. */
572
+ async refreshOpenDiagnostics(
573
+ options?: { maxWaitMs?: number; quietMs?: number } & CodeRequestControl,
574
+ ): Promise<DiagnosticEvidenceSummary> {
575
+ const summary = await refreshOpenDiagnosticsForClients(this.clients.values(), options);
576
+ return this.toWorkspaceDiagnosticEvidence(summary.documents);
413
577
  }
414
578
 
415
579
  /** Clear cached pull-diagnostic result IDs across all clients. */
@@ -426,21 +590,57 @@ export class LspManager {
426
590
  }
427
591
  }
428
592
 
593
+ /**
594
+ * Record one workspace invalidation event and forward it to clients.
595
+ *
596
+ * Every call with a non-empty change batch advances the invalidation
597
+ * generation, so a repeated edit to the same file opens a fresh
598
+ * generation. Recovery passes forward changes through
599
+ * {@link notifyWorkspaceFileChanges} and never advance the generation.
600
+ */
601
+ noteWorkspaceChanges(changes: FileEvent[]): void {
602
+ if (changes.length > 0) this.invalidationEpoch++;
603
+ this.notifyWorkspaceFileChanges(changes);
604
+ }
605
+
606
+ /** Return per-route diagnostic evidence capability for recovery targeting. */
607
+ getClientDiagnosticRoutes(): Array<{
608
+ key: string;
609
+ supportsPull: boolean;
610
+ unconfirmedFiles: string[];
611
+ stallSignal: ReturnType<LspClient["getRecoveryStallSignal"]>;
612
+ }> {
613
+ const routes: Array<{
614
+ key: string;
615
+ supportsPull: boolean;
616
+ unconfirmedFiles: string[];
617
+ stallSignal: ReturnType<LspClient["getRecoveryStallSignal"]>;
618
+ }> = [];
619
+ for (const [key, client] of this.clients) {
620
+ if (client.status !== "running") continue;
621
+ const unconfirmedFiles = client
622
+ .getDiagnosticSnapshot()
623
+ .documents.filter((document) => document.status === "unconfirmed")
624
+ .map((document) => uriToFile(document.uri))
625
+ .filter((file) => this.isDiagnosticFile(file));
626
+ routes.push({
627
+ key,
628
+ supportsPull: client.hasDiagnosticProvider,
629
+ unconfirmedFiles,
630
+ stallSignal: client.getRecoveryStallSignal(),
631
+ });
632
+ }
633
+ return routes;
634
+ }
635
+
429
636
  /** Force a workspace-wide diagnostic recovery pass. */
430
637
  async recoverWorkspaceDiagnostics(options?: {
431
638
  changes?: FileEvent[];
432
639
  restartIfStillStale?: boolean;
433
640
  maxWaitMs?: number;
434
641
  quietMs?: number;
435
- }): Promise<{
436
- attemptedClients: number;
437
- restartedClients: number;
438
- staleAssessment: {
439
- suspected: boolean;
440
- matchedFiles: Array<{ file: string; diagnostics: Diagnostic[] }>;
441
- warning: string | null;
442
- };
443
- }> {
642
+ control?: CodeRequestControl;
643
+ }): Promise<WorkspaceRecoveryResult> {
444
644
  return recoverWorkspaceDiagnosticsImpl(this, options);
445
645
  }
446
646
 
@@ -448,7 +648,12 @@ export class LspManager {
448
648
  async shutdownAll(): Promise<void> {
449
649
  const shutdowns = Array.from(this.clients.values()).map((c) => c.shutdown().catch(() => {}));
450
650
  await Promise.all(shutdowns);
651
+ // Dispose every client so pending or active progress tokens and
652
+ // readiness promises cannot outlive the manager.
653
+ for (const client of this.clients.values()) client.dispose();
451
654
  this.clients.clear();
655
+ this.clientGenerations.clear();
656
+ this.recoveryRestartEpochs.clear();
452
657
  this.unavailable.clear();
453
658
  this.knownRoots.clear();
454
659
  this.warmedWorkspaceSymbolProjects.clear();
@@ -456,6 +661,30 @@ export class LspManager {
456
661
  this.pendingWarmProbes.clear();
457
662
  }
458
663
  /** Get status of all servers. */
664
+ getRunningClientCount(): number {
665
+ return Array.from(this.clients.values()).filter((client) => client.status === "running").length;
666
+ }
667
+
668
+ /** Get the server names of all running clients, for recovery telemetry. */
669
+ getRunningClientNames(): string[] {
670
+ return Array.from(this.clients.values())
671
+ .filter((client) => client.status === "running")
672
+ .map((client) => client.name);
673
+ }
674
+
675
+ /** Check whether a path belongs to the configured diagnostic evidence scope. */
676
+ isDiagnosticFile(filePath: string): boolean {
677
+ const relative = path.relative(this.cwd, path.resolve(this.cwd, filePath));
678
+ return (
679
+ !shouldIgnoreLspPath(relative, this.cwd) &&
680
+ !isExcludedByPattern(relative, this.excludePatterns)
681
+ );
682
+ }
683
+
684
+ getDiagnosticEvidence(): DiagnosticEvidenceSummary {
685
+ return this.getDiagnosticSnapshot().evidence;
686
+ }
687
+
459
688
  getStatus(): ManagerStatus {
460
689
  this.pruneMissingFiles();
461
690
  const servers: ServerStatus[] = [];
@@ -540,39 +769,65 @@ export class LspManager {
540
769
  }
541
770
  /** Get a diagnostic summary across all servers and files. */
542
771
  getDiagnosticSummary(): DiagnosticSummary[] {
772
+ return this.getDiagnosticSnapshot().entries;
773
+ }
774
+ getDiagnosticSnapshot(): {
775
+ entries: DiagnosticSummary[];
776
+ current: boolean;
777
+ evidence: DiagnosticEvidenceSummary;
778
+ } {
543
779
  this.pruneMissingFiles();
544
780
  const fileDiags = new Map<string, { errors: number; warnings: number }>();
781
+ const evidenceDocuments: DiagnosticEvidenceDocument[] = [];
782
+ let clientCurrent = true;
545
783
  for (const client of this.clients.values()) {
546
- for (const entry of client.getAllDiagnostics()) {
784
+ const snapshot = client.getDiagnosticSnapshot();
785
+ clientCurrent &&= snapshot.current;
786
+ for (const document of snapshot.documents) {
787
+ evidenceDocuments.push({
788
+ file: relativeFilePathFromUri(document.uri, this.cwd),
789
+ status: document.status,
790
+ });
791
+ }
792
+ for (const entry of snapshot.entries) {
547
793
  collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd, this.excludePatterns);
548
794
  }
549
795
  }
550
- return Array.from(fileDiags.entries()).map(([file, counts]) => ({ file, ...counts }));
796
+ const evidence = this.toWorkspaceDiagnosticEvidence(evidenceDocuments);
797
+ return {
798
+ entries: Array.from(fileDiags.entries()).map(([file, counts]) => ({ file, ...counts })),
799
+ current:
800
+ clientCurrent && evidence.documents.every((document) => document.status === "confirmed"),
801
+ evidence,
802
+ };
551
803
  }
552
804
  /** Get outstanding diagnostics at or above the configured inline threshold. */
553
805
  getOutstandingDiagnosticSummary(maxSeverity: number = 1): OutstandingDiagnosticSummaryEntry[] {
806
+ return this.getOutstandingDiagnosticSummarySnapshot(maxSeverity).entries;
807
+ }
808
+ getOutstandingDiagnosticSummarySnapshot(maxSeverity: number = 1): {
809
+ entries: OutstandingDiagnosticSummaryEntry[];
810
+ current: boolean;
811
+ evidence: DiagnosticEvidenceSummary;
812
+ } {
554
813
  this.pruneMissingFiles();
555
- const fileDiags = new Map<string, OutstandingDiagnosticSummaryEntry>();
556
- for (const client of this.clients.values()) {
557
- for (const entry of client.getAllDiagnostics()) {
558
- const file = relativeFilePathFromUri(entry.uri, this.cwd);
559
- if (shouldIgnoreLspPath(file, this.cwd)) continue;
560
- if (isExcludedByPattern(file, this.excludePatterns)) continue;
561
- const current = fileDiags.get(file) ?? createOutstandingDiagnosticSummary(file);
562
- const next = accumulateOutstandingDiagnostics(current, entry.diagnostics, maxSeverity);
563
- if (next.total > 0) {
564
- fileDiags.set(file, next);
565
- }
566
- }
567
- }
568
- return Array.from(fileDiags.values()).sort(
569
- (a, b) =>
570
- b.errors - a.errors ||
571
- b.warnings - a.warnings ||
572
- b.information - a.information ||
573
- b.hints - a.hints ||
574
- a.file.localeCompare(b.file),
814
+ const evidence = this.toWorkspaceDiagnosticEvidence(
815
+ collectClientDiagnosticEvidenceDocuments(this.clients.values(), this.cwd),
816
+ );
817
+ const clientCurrent = Array.from(this.clients.values()).every(
818
+ (client) => client.getDiagnosticSnapshot().current,
575
819
  );
820
+ return {
821
+ entries: collectOutstandingDiagnosticSummaryEntries(
822
+ this.clients.values(),
823
+ this.cwd,
824
+ this.excludePatterns,
825
+ maxSeverity,
826
+ ),
827
+ current:
828
+ clientCurrent && evidence.documents.every((document) => document.status === "confirmed"),
829
+ evidence,
830
+ };
576
831
  }
577
832
  getRelevantOutstandingDiagnosticsSummaryText(
578
833
  relevantPaths: string[],
@@ -591,28 +846,71 @@ export class LspManager {
591
846
  getOutstandingDiagnostics(
592
847
  maxSeverity: number = 1,
593
848
  ): Array<{ file: string; diagnostics: Diagnostic[] }> {
849
+ return this.getOutstandingDiagnosticsSnapshot(maxSeverity).entries;
850
+ }
851
+ getOutstandingDiagnosticsSnapshot(maxSeverity: number = 1): {
852
+ entries: Array<{ file: string; diagnostics: Diagnostic[] }>;
853
+ current: boolean;
854
+ evidence: DiagnosticEvidenceSummary;
855
+ } {
594
856
  this.pruneMissingFiles();
595
- return collectOutstandingDiagnosticsDetailed(
596
- this.clients.values(),
597
- this.cwd,
598
- this.excludePatterns,
599
- maxSeverity,
857
+ const clients = Array.from(this.clients.values());
858
+ let clientCurrent = true;
859
+ const evidenceDocuments = clients.flatMap((client) => {
860
+ const snapshot = client.getDiagnosticSnapshot();
861
+ clientCurrent &&= snapshot.current;
862
+ return snapshot.documents.map((document) => ({
863
+ file: relativeFilePathFromUri(document.uri, this.cwd),
864
+ status: document.status,
865
+ }));
866
+ });
867
+ const evidence = this.toWorkspaceDiagnosticEvidence(evidenceDocuments);
868
+ return {
869
+ entries: collectOutstandingDiagnosticsDetailed(
870
+ clients,
871
+ this.cwd,
872
+ this.excludePatterns,
873
+ maxSeverity,
874
+ ),
875
+ current:
876
+ clientCurrent && evidence.documents.every((document) => document.status === "confirmed"),
877
+ evidence,
878
+ };
879
+ }
880
+ private toWorkspaceDiagnosticEvidence(
881
+ documents: readonly DiagnosticEvidenceDocument[],
882
+ ): DiagnosticEvidenceSummary {
883
+ const byFile = new Map<string, DiagnosticEvidenceDocument>();
884
+ for (const document of documents) {
885
+ const file = path.relative(this.cwd, path.resolve(this.cwd, document.file));
886
+ if (shouldIgnoreLspPath(file, this.cwd)) continue;
887
+ if (isExcludedByPattern(file, this.excludePatterns)) continue;
888
+ const existing = byFile.get(file);
889
+ if (!existing || evidenceStatusRank(document.status) > evidenceStatusRank(existing.status)) {
890
+ byFile.set(file, { file, status: document.status });
891
+ }
892
+ }
893
+ return summarizeDiagnosticEvidence(
894
+ Array.from(byFile.values()).sort((a, b) => a.file.localeCompare(b.file)),
600
895
  );
601
896
  }
897
+
602
898
  async workspaceSymbol(
603
899
  query: string,
900
+ control?: CodeRequestControl,
604
901
  ): Promise<CodeQueryResult<(SymbolInformation | WorkspaceSymbol)[]>> {
605
- const initial = await collectWorkspaceSymbols(this.clients.values(), query);
902
+ const initial = await collectWorkspaceSymbols(this.clients.values(), query, control);
606
903
  if (!initial.hasSupport || initial.results.length > 0) {
607
904
  return workspaceSymbolCollectionResult(initial);
608
905
  }
609
906
 
610
- const warmed = await this.warmWorkspaceSymbolProjectsUntilResult(
611
- findWorkspaceSymbolWarmTargets,
612
- getWorkspaceSymbolWarmPosition,
613
- collectWorkspaceSymbols,
907
+ const warmed = await this.warmWorkspaceSymbolProjectsUntilResult({
908
+ findWarmTargets: findWorkspaceSymbolWarmTargets,
909
+ getWarmPosition: getWorkspaceSymbolWarmPosition,
910
+ collect: (clients, value) => collectWorkspaceSymbols(clients, value, control),
614
911
  query,
615
- );
912
+ control,
913
+ });
616
914
  return workspaceSymbolCollectionResult(warmed.collection ?? initial);
617
915
  }
618
916
  async ensureFileOpen(filePath: string): Promise<LspClient | null> {
@@ -629,18 +927,20 @@ export class LspManager {
629
927
  }
630
928
 
631
929
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: warm-up coordinates client/project iteration, targeted semantic nudges, and early-return queries in one place.
632
- private async warmWorkspaceSymbolProjectsUntilResult(
930
+ private async warmWorkspaceSymbolProjectsUntilResult(options: {
633
931
  findWarmTargets: (
634
932
  root: string,
635
933
  rootMarkers: string[],
636
934
  fileTypes: string[],
637
- ) => Array<{ projectRoot: string; file: string }>,
935
+ ) => Array<{ projectRoot: string; file: string }>;
638
936
  getWarmPosition: (
639
937
  symbols: import("../config/types.ts").DocumentSymbol[] | SymbolInformation[] | null,
640
- ) => import("../config/types.ts").Position | null,
641
- collect: (clients: Iterable<LspClient>, query: string) => Promise<WorkspaceSymbolCollection>,
642
- query: string,
643
- ): Promise<{ warmedAny: boolean; collection: WorkspaceSymbolCollection | null }> {
938
+ ) => import("../config/types.ts").Position | null;
939
+ collect: (clients: Iterable<LspClient>, query: string) => Promise<WorkspaceSymbolCollection>;
940
+ query: string;
941
+ control?: CodeRequestControl;
942
+ }): Promise<{ warmedAny: boolean; collection: WorkspaceSymbolCollection | null }> {
943
+ const { findWarmTargets, getWarmPosition, collect, query, control } = options;
644
944
  let warmedAny = false;
645
945
 
646
946
  for (const client of Array.from(this.clients.values())) {
@@ -667,13 +967,20 @@ export class LspManager {
667
967
  const openedClient = await this.ensureFileOpen(target.file);
668
968
  if (!openedClient) continue;
669
969
 
970
+ // A cancelled pass must not mark projects warmed on its way out.
971
+ throwIfCodeRequestInterrupted(control);
670
972
  this.warmedWorkspaceSymbolProjects.add(projectKey);
671
973
  warmedAny = true;
672
974
 
673
- const symbols = await openedClient.documentSymbols(target.file);
975
+ const symbols = control
976
+ ? await openedClient.documentSymbols(target.file, control)
977
+ : await openedClient.documentSymbols(target.file);
674
978
  if (symbols.kind !== "unavailable") {
675
979
  const hoverPosition = getWarmPosition(symbols.data);
676
- if (hoverPosition) await openedClient.hover(target.file, hoverPosition);
980
+ if (hoverPosition) {
981
+ if (control) await openedClient.hover(target.file, hoverPosition, control);
982
+ else await openedClient.hover(target.file, hoverPosition);
983
+ }
677
984
  }
678
985
 
679
986
  const collected = await collect(this.clients.values(), query);
@@ -690,6 +997,7 @@ export class LspManager {
690
997
  client: LspClient,
691
998
  filePath: string,
692
999
  preferExactFile: boolean = false,
1000
+ control?: CodeRequestControl,
693
1001
  ): Promise<void> {
694
1002
  const serverConfig = this.config.servers[client.name];
695
1003
  if (!serverConfig) return;
@@ -706,14 +1014,16 @@ export class LspManager {
706
1014
 
707
1015
  const pending = this.pendingWarmProbes.get(projectKey);
708
1016
  if (pending) {
709
- await pending;
1017
+ // The shared probe keeps running for other consumers; only this
1018
+ // caller's wait stops when it cancels.
1019
+ await raceRequestControl(pending, control);
710
1020
  return;
711
1021
  }
712
1022
 
713
- const probe = this.performWarmProbe(client, resolvedFile, projectKey);
1023
+ const probe = this.performWarmProbe(client, resolvedFile, projectKey, control);
714
1024
  this.pendingWarmProbes.set(projectKey, probe);
715
1025
  try {
716
- await probe;
1026
+ await raceRequestControl(probe, control);
717
1027
  } finally {
718
1028
  this.pendingWarmProbes.delete(projectKey);
719
1029
  }
@@ -723,16 +1033,23 @@ export class LspManager {
723
1033
  _client: LspClient,
724
1034
  resolvedFile: string,
725
1035
  projectKey: string,
1036
+ control?: CodeRequestControl,
726
1037
  ): Promise<void> {
727
1038
  const openedClient = await this.ensureFileOpen(resolvedFile);
728
1039
  if (!openedClient) return;
729
1040
 
730
1041
  this.warmedSemanticProjects.add(projectKey);
731
1042
 
732
- const symbols = await openedClient.documentSymbols(resolvedFile);
1043
+ // The probe is shared state for concurrent callers. One caller's
1044
+ // cancellation must not cancel the shared in-flight queries, so only the
1045
+ // Debug Operation ID is forwarded; each caller races its own wait above.
1046
+ const sharedControl = { operationId: control?.operationId };
1047
+ const symbols = await openedClient.documentSymbols(resolvedFile, sharedControl);
733
1048
  if (symbols.kind === "unavailable") return;
734
1049
  const hoverPosition = getWorkspaceSymbolWarmPosition(symbols.data);
735
- if (hoverPosition) await openedClient.hover(resolvedFile, hoverPosition);
1050
+ if (hoverPosition) {
1051
+ await openedClient.hover(resolvedFile, hoverPosition, sharedControl);
1052
+ }
736
1053
  }
737
1054
 
738
1055
  private hasOpenFileInProject(client: LspClient, projectRoot: string): boolean {
@@ -770,3 +1087,54 @@ export class LspManager {
770
1087
  }
771
1088
  }
772
1089
  }
1090
+
1091
+ function collectClientDiagnosticEvidenceDocuments(
1092
+ clients: Iterable<LspClient>,
1093
+ cwd: string,
1094
+ ): DiagnosticEvidenceDocument[] {
1095
+ return Array.from(clients).flatMap((client) =>
1096
+ client.getDiagnosticSnapshot().documents.map((document) => ({
1097
+ file: relativeFilePathFromUri(document.uri, cwd),
1098
+ status: document.status,
1099
+ })),
1100
+ );
1101
+ }
1102
+
1103
+ function collectOutstandingDiagnosticSummaryEntries(
1104
+ clients: Iterable<LspClient>,
1105
+ cwd: string,
1106
+ excludePatterns: string[],
1107
+ maxSeverity: number,
1108
+ ): OutstandingDiagnosticSummaryEntry[] {
1109
+ const fileDiags = new Map<string, OutstandingDiagnosticSummaryEntry>();
1110
+ for (const client of clients) {
1111
+ for (const entry of client.getDiagnosticSnapshot().entries) {
1112
+ const file = relativeFilePathFromUri(entry.uri, cwd);
1113
+ if (shouldIgnoreLspPath(file, cwd) || isExcludedByPattern(file, excludePatterns)) continue;
1114
+ const existing = fileDiags.get(file) ?? createOutstandingDiagnosticSummary(file);
1115
+ const next = accumulateOutstandingDiagnostics(existing, entry.diagnostics, maxSeverity);
1116
+ if (next.total > 0) fileDiags.set(file, next);
1117
+ }
1118
+ }
1119
+ return Array.from(fileDiags.values()).sort(
1120
+ (a, b) =>
1121
+ b.errors - a.errors ||
1122
+ b.warnings - a.warnings ||
1123
+ b.information - a.information ||
1124
+ b.hints - a.hints ||
1125
+ a.file.localeCompare(b.file),
1126
+ );
1127
+ }
1128
+
1129
+ function evidenceStatusRank(status: DiagnosticEvidenceDocument["status"]): number {
1130
+ switch (status) {
1131
+ case "confirmed":
1132
+ return 1;
1133
+ case "unconfirmed":
1134
+ return 2;
1135
+ case "failed":
1136
+ return 3;
1137
+ case "removed":
1138
+ return 4;
1139
+ }
1140
+ }