@herbertgao/pi-extensions 2026.8.4 → 2026.8.5

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 (288) hide show
  1. package/README.md +4 -4
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +5 -8
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  13. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  14. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  15. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  16. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +15 -2
  20. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  21. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  22. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  25. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  26. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  27. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  30. package/node_modules/@narumitw/pi-btw/README.md +8 -7
  31. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  32. package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
  33. package/node_modules/pi-lens/CHANGELOG.md +2440 -954
  34. package/node_modules/pi-lens/README.md +13 -0
  35. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  36. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  37. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  38. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  39. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  40. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  41. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  42. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  43. package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
  44. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  45. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  46. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  47. package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
  48. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  49. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  50. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  51. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  52. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  53. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  54. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  55. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  56. package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
  57. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  58. package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
  59. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  60. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  61. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  62. package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
  63. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
  64. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  65. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
  66. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  67. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  68. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  69. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  70. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  71. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
  72. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  73. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  74. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  75. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  85. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  86. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  87. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  99. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  100. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  101. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  102. package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
  103. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  104. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  105. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  106. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  107. package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
  108. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  109. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  110. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  111. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  112. package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
  113. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  114. package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
  115. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
  116. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  117. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  118. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  119. package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
  120. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  121. package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
  122. package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
  123. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  124. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  125. package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
  126. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  127. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  128. package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
  129. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  130. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  131. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
  132. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  133. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
  134. package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
  135. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  136. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
  137. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  138. package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
  139. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  140. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  141. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  142. package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
  143. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  144. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  145. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  146. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
  147. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  148. package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
  149. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  150. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  151. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  152. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  153. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  154. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  155. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  156. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  157. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  158. package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
  159. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  160. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  161. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  162. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  163. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  164. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  165. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  166. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  167. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  168. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  169. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  170. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  171. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  172. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  173. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  174. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  175. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  176. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  177. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  178. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  179. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  180. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  181. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  182. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
  183. package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
  184. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
  185. package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
  186. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  187. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
  188. package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
  189. package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
  190. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  191. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
  192. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  193. package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
  194. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  195. package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
  196. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  197. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  198. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  199. package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
  200. package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
  201. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  202. package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
  203. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  204. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  205. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  206. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  207. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  208. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  209. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  210. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  211. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  212. package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
  213. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  214. package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
  215. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  216. package/node_modules/pi-lens/dist/index.js +49361 -36888
  217. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  218. package/node_modules/pi-lens/dist/mcp/server.js +211 -44
  219. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  220. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
  221. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
  222. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  223. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  224. package/node_modules/pi-lens/docs/agent-guide.md +2 -2
  225. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  226. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  227. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  228. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
  229. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  230. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  231. package/node_modules/pi-lens/docs/fable.md +27 -0
  232. package/node_modules/pi-lens/docs/features.md +31 -2
  233. package/node_modules/pi-lens/docs/globalconfig.md +45 -1
  234. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  235. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  236. package/node_modules/pi-lens/docs/mcp.md +37 -4
  237. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  238. package/node_modules/pi-lens/docs/settings.md +12 -1
  239. package/node_modules/pi-lens/docs/tools.md +4 -1
  240. package/node_modules/pi-lens/docs/usage.md +18 -0
  241. package/node_modules/pi-lens/package.json +10 -10
  242. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  243. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  244. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  245. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  246. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  247. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  248. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  249. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  250. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  251. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  252. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  253. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  254. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
  255. package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
  256. package/node_modules/pi-mcp-adapter/README.md +34 -3
  257. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  258. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  259. package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
  260. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  261. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  262. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  263. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  264. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  265. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  266. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  267. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  268. package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
  269. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  270. package/node_modules/pi-mcp-adapter/package.json +3 -3
  271. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  272. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  273. package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
  274. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  275. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  276. package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
  277. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  278. package/node_modules/pi-mcp-adapter/types.ts +93 -27
  279. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  280. package/package.json +11 -12
  281. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  282. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  283. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  284. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  285. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  286. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  287. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  288. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -7,6 +7,7 @@
7
7
  * - Diagnostics with debouncing
8
8
  * - Request/response handling
9
9
  */
10
+ import { logExtension } from "../extension-log.js";
10
11
  import { spawn as nodeSpawn } from "node:child_process";
11
12
  import { EventEmitter } from "node:events";
12
13
  import { access, readFile } from "node:fs/promises";
@@ -18,7 +19,9 @@ import { logLatency } from "../latency-logger.js";
18
19
  // `./node` subpath (no `.js`); the old `/node.js` file path no longer resolves.
19
20
  import { CancellationTokenSource, createMessageConnection, StreamMessageReader, StreamMessageWriter, } from "../deps/vscode-jsonrpc.js";
20
21
  import { getAmbientAbortSignal } from "../safe-spawn.js";
21
- import { applyWorkspaceEdit } from "./edits.js";
22
+ import { hashDiagnosticContent, } from "./diagnostic-binding.js";
23
+ import { newLspMutationCorrelationId, } from "../lsp-mutation.js";
24
+ import { applyWorkspaceEdit, normalizeWorkspaceEditToUtf16, } from "./edits.js";
22
25
  import { recordLspChild, removeLspChild } from "../instance-registry.js";
23
26
  import { normalizeMapKey, uriToPath } from "./path-utils.js";
24
27
  import { ADVERTISED_POSITION_ENCODINGS, convertCharacterOffset, lineTextAt, negotiatePositionEncoding, } from "./position-encoding.js";
@@ -123,6 +126,25 @@ const PULL_DIAGNOSTICS_RETRY_INTERVAL_MS = positiveIntFromEnv("PI_LENS_LSP_PULL_
123
126
  // (per #240) is NOT read as clean and falls through to the bounded push backstop.
124
127
  const PULL_REQUEST_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_PULL_REQUEST_TIMEOUT_MS", 10_000);
125
128
  const SHUTDOWN_REQUEST_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_SHUTDOWN_TIMEOUT_MS", 1000);
129
+ // #1277: cheap liveness round-trip for the silent-clean gates (`index.ts`).
130
+ // Those gates convert a diagnostics-wait timeout into a confirmed-clean
131
+ // result from a STATIC capability classification (`silentOnClean`) alone —
132
+ // but a wedged server (accepted the notify write, then hung) satisfies that
133
+ // classification identically to a genuinely clean one. This is deliberately
134
+ // short relative to NAV_REQUEST_TIMEOUT_MS: it only needs to prove the
135
+ // connection round-trips SOMETHING before the touch reports clean, not
136
+ // complete a real navigation request.
137
+ const LIVENESS_PING_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_LIVENESS_PING_TIMEOUT_MS", 300);
138
+ // Distinctive, unlikely-to-collide query string — the response content is
139
+ // never inspected, only whether one arrived before the timeout.
140
+ const LIVENESS_PING_QUERY = "__pi_lens_liveness_ping__";
141
+ // #1104: bound on `state.workspacePullResultCache` — one entry per distinct
142
+ // file the server has ever returned a `resultId` for across this client's
143
+ // lifetime. A full clear on overflow (rather than an LRU) is fine, same
144
+ // reasoning as `DISK_BINDING_MEMO_MAX` in diagnostic-binding.ts: each entry is
145
+ // cheaply rebuilt by the next full pull, the worst case is just one extra full
146
+ // (non-`unchanged`) report per affected file.
147
+ const WORKSPACE_PULL_RESULT_CACHE_MAX = 4096;
126
148
  // Anti-deadlock backstop for workspace/executeCommand. Deliberately generous
127
149
  // (30s): the command is mutating and legitimately long-running (a real server
128
150
  // refactor / organize-imports), so this must not truncate valid work — it only
@@ -282,12 +304,34 @@ export async function killProcessTree(proc, pid, options = {}) {
282
304
  }
283
305
  };
284
306
  try {
307
+ // #1114: gate the escalation on OBSERVED exit, not `proc.killed`. Node
308
+ // only sets `proc.killed = true` when `proc.kill()` (the ChildProcess
309
+ // method) successfully SENDS a signal — never when the process actually
310
+ // dies, and the primary SIGTERM path above goes through the raw
311
+ // `process.kill(-pid, …)` process-group call, which never touches
312
+ // `proc.killed` at all. Checking `!proc.killed` here was therefore
313
+ // either always-true (unconditional SIGKILL after the window,
314
+ // regardless of whether the group already died — group-kill path) or
315
+ // always-false/dead (direct-child fallback path, same shape as the
316
+ // safe-spawn escalation bug). An `exit` listener set once, up front,
317
+ // gives a real observed-death signal for both. Seeded from the same
318
+ // `exitCode`/`signalCode` pre-check the top-of-function early return
319
+ // uses (:689) — that early return is skipped when
320
+ // `options.processExiting` is set, so a process that was ALREADY dead
321
+ // on entry can still reach here; without seeding, `exited` would stay
322
+ // false (the "exit" event already fired before this listener was
323
+ // attached) and the fast/non-fast branches below would still fire a
324
+ // redundant group SIGKILL at the escalation window.
325
+ let exited = proc.exitCode != null || proc.signalCode != null;
326
+ proc.once?.("exit", () => {
327
+ exited = true;
328
+ });
285
329
  if (!killPosixProcessGroup("SIGTERM")) {
286
330
  killDirectChild("SIGTERM");
287
331
  }
288
332
  if (options.fast) {
289
333
  const timer = setTimeout(() => {
290
- if (!proc.killed) {
334
+ if (!exited) {
291
335
  logLatency({
292
336
  type: "phase",
293
337
  phase: "lsp_kill_escalation",
@@ -324,7 +368,7 @@ export async function killProcessTree(proc, pid, options = {}) {
324
368
  }, 1500);
325
369
  proc.once?.("exit", onExit);
326
370
  });
327
- if (!exitedInTime && !proc.killed) {
371
+ if (!exitedInTime && !exited) {
328
372
  logLatency({
329
373
  type: "phase",
330
374
  phase: "lsp_kill_escalation",
@@ -397,9 +441,33 @@ function clearDiagnosticsForPath(state, normalizedPath) {
397
441
  state.documentPullDiagnostics?.delete(normalizedPath);
398
442
  state.documentPullDiagnosticTimestamps?.delete(normalizedPath);
399
443
  state.diagnosticDocVersions?.delete(normalizedPath);
444
+ // #1095: a cleared path must never serve a stale content binding alongside a
445
+ // later publish — drop it with the diagnostics it described. (The last-sent
446
+ // `documentContentHashes` record is intentionally retained: it describes what
447
+ // we sent, which the NEXT publish for that version still needs to bind to.)
448
+ state.diagnosticBindings?.delete(normalizedPath);
449
+ // #1104: a resync invalidates any `unchanged`-report basis too — the next
450
+ // pull must not inherit a resultId/contentHash computed against the
451
+ // content this resync just replaced.
452
+ state.pullResultIds?.delete(normalizedPath);
453
+ state.workspacePullResultCache?.delete(normalizedPath);
400
454
  legacy.diagnostics?.delete(normalizedPath);
401
455
  legacy.diagnosticTimestamps?.delete(normalizedPath);
402
456
  }
457
+ /**
458
+ * #1095: fingerprint the EXACT didOpen/didChange payload text at SEND time and
459
+ * tag it with the document version it was sent as, so a later
460
+ * `publishDiagnostics` echoing that version can bind its diagnostics to the
461
+ * content they were computed against. Runs on in-memory content — never a disk
462
+ * read on the notification path (I1). Bounded by the same file-size gates the
463
+ * caller already applies to the content it hands us.
464
+ */
465
+ function recordSentContent(state, normalizedPath, version, content) {
466
+ state.documentContentHashes.set(normalizedPath, {
467
+ version,
468
+ hash: hashDiagnosticContent(content),
469
+ });
470
+ }
403
471
  // Methods that can be registered dynamically and map to operationSupport keys
404
472
  const DYNAMIC_OPERATION_METHOD_MAP = {
405
473
  "textDocument/definition": "definition",
@@ -475,10 +543,20 @@ export function setupIncomingHandlers(state, initialization) {
475
543
  state.connection.onNotification("textDocument/publishDiagnostics", (params) => {
476
544
  const filePath = uriToPath(params.uri);
477
545
  const normalizedPath = normalizeMapKey(filePath);
546
+ // A server can flush a queued publish after didClose during teardown.
547
+ // Do not resurrect diagnostics or their content binding for a document
548
+ // that is no longer open on this client.
549
+ if (state.closedDocuments?.has(normalizedPath))
550
+ return;
478
551
  const newDiags = normalizeLspDiagnostics(params.diagnostics || []);
479
552
  const docVersion = params.version;
480
553
  if (PUB_DEBUG) {
481
- console.error(`[lsp-pub] server=${state.serverId} pubVersion=${docVersion} docVersion=${state.documentVersions?.get(normalizedPath)} diags=${newDiags.length}`);
554
+ // #1333: PUB_DEBUG gate preserved; sink is extension.log.
555
+ logExtension({
556
+ subsystem: "lsp-pub",
557
+ level: "debug",
558
+ message: `server=${state.serverId} pubVersion=${docVersion} docVersion=${state.documentVersions?.get(normalizedPath)} diags=${newDiags.length}`,
559
+ });
482
560
  }
483
561
  const strategy = getStrategy(state.serverId);
484
562
  // Record the document version these diagnostics were computed against
@@ -488,6 +566,26 @@ export function setupIncomingHandlers(state, initialization) {
488
566
  if (docVersion !== undefined) {
489
567
  state.diagnosticDocVersions.set(normalizedPath, docVersion);
490
568
  }
569
+ recordBinding();
570
+ };
571
+ // #1095: bind the just-stored diagnostics to the content they were
572
+ // computed against. Only when the server reported a version AND we still
573
+ // hold the sent-content fingerprint for exactly that version — otherwise
574
+ // no contentHash is recorded, so the binding reads "unknown" and a
575
+ // version-less server behaves exactly as before. Runs at the same
576
+ // write-time moment as `pushDiagnostics.set` (superseded pushes are
577
+ // dropped before this via `isSupersededPush`, so a binding never lags the
578
+ // latest sent version).
579
+ const recordBinding = () => {
580
+ if (docVersion === undefined) {
581
+ state.diagnosticBindings.delete(normalizedPath);
582
+ return;
583
+ }
584
+ const sent = state.documentContentHashes.get(normalizedPath);
585
+ state.diagnosticBindings.set(normalizedPath, {
586
+ version: docVersion,
587
+ contentHash: sent && sent.version === docVersion ? sent.hash : undefined,
588
+ });
491
589
  };
492
590
  // Late/superseded-push guard: if the server stamped this push with a
493
591
  // version and that version already lags the latest didChange we sent,
@@ -580,8 +678,21 @@ export function setupIncomingHandlers(state, initialization) {
580
678
  if (state.serverEditsAllowed <= 0 || !params?.edit) {
581
679
  return { applied: false, failureReason: "edit not solicited" };
582
680
  }
681
+ const context = (state.activeMutationDepth ?? 0) === 1
682
+ ? state.activeMutationContext
683
+ : undefined;
684
+ const telemetryContext = context ?? {
685
+ cwd: state.root,
686
+ correlationId: newLspMutationCorrelationId(),
687
+ tool: "lsp-workspace-applyEdit",
688
+ source: "lsp-edit",
689
+ };
583
690
  try {
584
- await applyWorkspaceEdit(params.edit, state.root);
691
+ await applyWorkspaceEdit(params.edit, state.root, {
692
+ positionEncoding: state.positionEncoding,
693
+ documentVersions: state.documentVersions,
694
+ mutationContext: telemetryContext,
695
+ });
585
696
  return { applied: true };
586
697
  }
587
698
  catch (err) {
@@ -603,16 +714,30 @@ export function setupIncomingHandlers(state, initialization) {
603
714
  });
604
715
  state.connection.onRequest("window/workDoneProgress/create", async () => { });
605
716
  }
717
+ /**
718
+ * #1127: record the FIRST moment this client observed its own death. Detection
719
+ * of a dead client (the next `getClientForFile` attach in index.ts) can happen
720
+ * long after the process actually died — this timestamp is the only reliable
721
+ * "when did it die" signal, so it must be set here, at the earliest death
722
+ * signal, not derived later from detection time.
723
+ */
724
+ function markExitedIfUnset(state) {
725
+ if (state.exitedAt === undefined) {
726
+ state.exitedAt = Date.now();
727
+ }
728
+ }
606
729
  function setupConnectionLifecycle(state, recentStderr) {
607
730
  state.connection.onError(([error]) => {
608
731
  state.lastError = error instanceof Error ? error : new Error(String(error));
609
732
  state.isConnected = false;
610
733
  state.isDestroyed = true;
734
+ markExitedIfUnset(state);
611
735
  disposeClientConnection(state);
612
736
  });
613
737
  state.connection.onClose(() => {
614
738
  state.isConnected = false;
615
739
  state.isDestroyed = true;
740
+ markExitedIfUnset(state);
616
741
  disposeClientConnection(state);
617
742
  });
618
743
  state.lspProcess.process.on("exit", (code, signal) => {
@@ -627,6 +752,7 @@ function setupConnectionLifecycle(state, recentStderr) {
627
752
  const wasIntentional = state.shutdownRequested;
628
753
  state.isConnected = false;
629
754
  state.isDestroyed = true;
755
+ markExitedIfUnset(state);
630
756
  disposeClientConnection(state);
631
757
  if (!wasIntentional) {
632
758
  logLatency({
@@ -649,6 +775,12 @@ async function clientRequestPullDiagnostics(state, filePath, budgetMs = PULL_REQ
649
775
  if (!isClientAlive(state))
650
776
  return { status: "unavailable" };
651
777
  const uri = pathToFileURL(filePath).href;
778
+ const normalizedPath = normalizeMapKey(filePath);
779
+ // #1104: echo the last resultId we hold for this document so a server that
780
+ // hasn't changed its view can answer `kind: "unchanged"` instead of
781
+ // recomputing — see the `kind === "unchanged"` branch below for how that's
782
+ // honored (inherit, never treat an omitted `items` as clean).
783
+ const previousResultId = state.pullResultIds.get(normalizedPath);
652
784
  try {
653
785
  // withTimeout is the backstop against a hung pull-mode server: without it
654
786
  // this await never settles unless the stream is destroyed. Bounded by the
@@ -657,23 +789,69 @@ async function clientRequestPullDiagnostics(state, filePath, budgetMs = PULL_REQ
657
789
  // `clean`), so it falls through to the push-wait/timeout backstop.
658
790
  const report = await withTimeout(safeSendRequest(state.connection, "textDocument/diagnostic", {
659
791
  textDocument: { uri },
792
+ ...(previousResultId !== undefined && { previousResultId }),
660
793
  }), Math.max(1, Math.min(PULL_REQUEST_TIMEOUT_MS, budgetMs)));
661
- if (!report)
794
+ if (!report) {
795
+ recordPullFailure(state, "textDocument/diagnostic", new Error("empty response"));
662
796
  return { status: "unavailable" };
663
- const normalizedPath = normalizeMapKey(filePath);
664
- const primaryItems = normalizeLspDiagnostics(report.items ?? []);
797
+ }
665
798
  const now = Date.now();
666
- state.documentPullDiagnostics.set(normalizedPath, primaryItems);
667
- state.documentPullDiagnosticTimestamps.set(normalizedPath, now);
668
- state.diagnosticsVersion += 1;
669
- let totalCount = primaryItems.length;
799
+ // #1104: the fingerprint of the content we last sent for this document —
800
+ // the SAME `documentContentHashes` entry the push binding path uses
801
+ // (`recordSentContent` runs unconditionally on every didOpen/didChange,
802
+ // regardless of push/pull mode), so this costs no extra read. A pull
803
+ // response describes whatever the server had when it answered, which for
804
+ // a pi-lens-opened document is exactly that last-sent payload.
805
+ const sentHash = state.documentContentHashes.get(normalizedPath)?.hash;
806
+ let totalCount;
807
+ if (report.kind === "unchanged") {
808
+ // #1104: same resultId basis as last time — an omitted `items` here
809
+ // means "no change", NOT "clean". Overwriting with `[]` would be the
810
+ // exact false-clean shape #570/#571 already fixed for the touch path;
811
+ // keep the previously stored diagnostics and binding as-is.
812
+ totalCount = state.documentPullDiagnostics.get(normalizedPath)?.length ?? 0;
813
+ // Still a fresh confirmation as of `now` even though the content is
814
+ // unchanged — bump the timestamp so `getAllDiagnostics()` doesn't read
815
+ // this entry as aging purely because the server had nothing new to say.
816
+ state.documentPullDiagnosticTimestamps.set(normalizedPath, now);
817
+ }
818
+ else {
819
+ const primaryItems = normalizeLspDiagnostics(report.items ?? []);
820
+ state.documentPullDiagnostics.set(normalizedPath, primaryItems);
821
+ state.documentPullDiagnosticTimestamps.set(normalizedPath, now);
822
+ state.diagnosticsVersion += 1;
823
+ state.diagnosticBindings.set(normalizedPath, { contentHash: sentHash });
824
+ totalCount = primaryItems.length;
825
+ }
826
+ if (report.resultId !== undefined) {
827
+ state.pullResultIds.set(normalizedPath, report.resultId);
828
+ }
829
+ else {
830
+ state.pullResultIds.delete(normalizedPath);
831
+ }
670
832
  if (report.relatedDocuments) {
671
833
  for (const [relatedUri, related] of Object.entries(report.relatedDocuments)) {
672
834
  const relatedPath = uriToPath(relatedUri);
673
- const relatedItems = normalizeLspDiagnostics(related?.items ?? []);
674
- state.documentPullDiagnostics.set(normalizeMapKey(relatedPath), relatedItems);
675
- state.documentPullDiagnosticTimestamps.set(normalizeMapKey(relatedPath), now);
676
- totalCount += relatedItems.length;
835
+ const relatedNormalized = normalizeMapKey(relatedPath);
836
+ if (related?.kind === "unchanged") {
837
+ totalCount +=
838
+ state.documentPullDiagnostics.get(relatedNormalized)?.length ?? 0;
839
+ state.documentPullDiagnosticTimestamps.set(relatedNormalized, now);
840
+ }
841
+ else {
842
+ const relatedItems = normalizeLspDiagnostics(related?.items ?? []);
843
+ state.documentPullDiagnostics.set(relatedNormalized, relatedItems);
844
+ state.documentPullDiagnosticTimestamps.set(relatedNormalized, now);
845
+ // #1104: a related document's diagnostics were NOT computed against
846
+ // content we independently sent/fingerprinted (we never requested
847
+ // it directly) — its binding stays honestly "unknown" rather than
848
+ // borrowing the primary document's hash.
849
+ state.diagnosticBindings.delete(relatedNormalized);
850
+ totalCount += relatedItems.length;
851
+ }
852
+ if (related?.resultId !== undefined) {
853
+ state.pullResultIds.set(relatedNormalized, related.resultId);
854
+ }
677
855
  }
678
856
  }
679
857
  state.diagnosticEmitter.emit("diagnostics", normalizedPath);
@@ -681,10 +859,34 @@ async function clientRequestPullDiagnostics(state, filePath, budgetMs = PULL_REQ
681
859
  ? { status: "found", count: totalCount }
682
860
  : { status: "clean" };
683
861
  }
684
- catch {
862
+ catch (err) {
863
+ recordPullFailure(state, "textDocument/diagnostic", err);
685
864
  return { status: "unavailable" };
686
865
  }
687
866
  }
867
+ const PULL_FAILURE_HISTORY_LIMIT = 10;
868
+ function recordPullFailure(state, method, error) {
869
+ const candidate = error;
870
+ const message = typeof candidate.message === "string" ? candidate.message : "";
871
+ const unsupportedMessage = /^(?:method not found|unknown method|unsupported method)(?::|$)/i;
872
+ if (candidate.code === -32601 ||
873
+ candidate.code === "-32601" ||
874
+ unsupportedMessage.test(message.trim()))
875
+ return;
876
+ state.pullFailureHistory.push({
877
+ timestamp: Date.now(),
878
+ method,
879
+ ...(typeof candidate.code === "number" || typeof candidate.code === "string"
880
+ ? { code: candidate.code }
881
+ : {}),
882
+ message: typeof candidate.message === "string"
883
+ ? candidate.message
884
+ : String(error),
885
+ });
886
+ if (state.pullFailureHistory.length > PULL_FAILURE_HISTORY_LIMIT) {
887
+ state.pullFailureHistory.splice(0, state.pullFailureHistory.length - PULL_FAILURE_HISTORY_LIMIT);
888
+ }
889
+ }
688
890
  /**
689
891
  * One project-wide `workspace/diagnostic` pull — a single request that returns
690
892
  * diagnostics for every document the server knows, instead of opening N files.
@@ -698,23 +900,73 @@ export async function clientRequestWorkspaceDiagnostics(state, budgetMs) {
698
900
  if (!state.workspaceDiagnosticsSupport.workspaceDiagnostics)
699
901
  return undefined;
700
902
  try {
701
- const report = await withTimeout(safeSendRequest(state.connection, "workspace/diagnostic", { previousResultIds: [] }), Math.max(1, budgetMs));
903
+ // #1104: echo every resultId we hold from a PRIOR pull so the server can
904
+ // answer `kind: "unchanged"` for files it hasn't recomputed, instead of
905
+ // resending (and us re-hashing) every file on every sweep.
906
+ const previousResultIds = Array.from(state.workspacePullResultCache.values()).map((entry) => ({ uri: entry.uri, value: entry.resultId }));
907
+ const report = await withTimeout(safeSendRequest(state.connection, "workspace/diagnostic", { previousResultIds }), Math.max(1, budgetMs));
702
908
  if (!report || !Array.isArray(report.items))
703
909
  return undefined;
704
910
  const out = [];
705
911
  for (const item of report.items) {
706
- // Only "full" reports carry items; "unchanged" means "same as last pull"
707
- // (none, since previousResultIds is empty on this one-shot request).
708
- if (!item?.uri || item.kind !== "full")
912
+ if (!item?.uri)
709
913
  continue;
710
- out.push({
711
- filePath: uriToPath(item.uri),
712
- diagnostics: normalizeLspDiagnostics(item.items ?? []),
713
- });
914
+ const filePath = uriToPath(item.uri);
915
+ const normalizedPath = normalizeMapKey(filePath);
916
+ if (item.kind === "unchanged") {
917
+ // #1104: inherit the prior pull's diagnostics + content binding for
918
+ // the SAME resultId basis — an "unchanged" report never carries
919
+ // `items`, so without this a file the server confirmed unchanged
920
+ // would silently drop out of the sweep result entirely.
921
+ const prior = state.workspacePullResultCache.get(normalizedPath);
922
+ if (!prior)
923
+ continue; // no earlier basis to inherit — nothing to report
924
+ if (item.resultId !== undefined) {
925
+ state.workspacePullResultCache.set(normalizedPath, {
926
+ ...prior,
927
+ resultId: item.resultId,
928
+ });
929
+ }
930
+ out.push({
931
+ filePath,
932
+ diagnostics: prior.diagnostics,
933
+ contentHash: prior.contentHash,
934
+ });
935
+ continue;
936
+ }
937
+ // "full" (or a non-conforming server omitting `kind`, per the LSP
938
+ // default) — recompute and re-fingerprint.
939
+ const diagnostics = normalizeLspDiagnostics(item.items ?? []);
940
+ // #1104: fingerprint the file bytes active AT REQUEST TIME. Best-effort —
941
+ // a read failure (deleted/unreadable mid-sweep) just leaves contentHash
942
+ // undefined, so the binding reads honestly "unknown", never fabricated.
943
+ let contentHash;
944
+ try {
945
+ contentHash = hashDiagnosticContent(await readFile(filePath, "utf-8"));
946
+ }
947
+ catch {
948
+ contentHash = undefined;
949
+ }
950
+ if (item.resultId !== undefined) {
951
+ if (state.workspacePullResultCache.size >= WORKSPACE_PULL_RESULT_CACHE_MAX) {
952
+ state.workspacePullResultCache.clear();
953
+ }
954
+ state.workspacePullResultCache.set(normalizedPath, {
955
+ uri: item.uri,
956
+ resultId: item.resultId,
957
+ diagnostics,
958
+ contentHash,
959
+ });
960
+ }
961
+ else {
962
+ state.workspacePullResultCache.delete(normalizedPath);
963
+ }
964
+ out.push({ filePath, diagnostics, contentHash });
714
965
  }
715
966
  return out;
716
967
  }
717
- catch {
968
+ catch (err) {
969
+ recordPullFailure(state, "workspace/diagnostic", err);
718
970
  return undefined;
719
971
  }
720
972
  }
@@ -807,8 +1059,8 @@ export async function clientWaitForDiagnostics(state, filePath, timeoutMs, optio
807
1059
  export async function handleNotifyOpen(state, filePath, content, languageId, preserveDiagnostics = false, silent = false) {
808
1060
  if (!isClientAlive(state))
809
1061
  return;
810
- const uri = pathToFileURL(filePath).href;
811
1062
  const normalizedPath = normalizeMapKey(filePath);
1063
+ const uri = state.openDocumentUris?.get(normalizedPath) ?? pathToFileURL(filePath).href;
812
1064
  if (state.openDocuments.has(normalizedPath) ||
813
1065
  state.pendingOpens.has(normalizedPath)) {
814
1066
  const version = (state.documentVersions.get(normalizedPath) ?? 0) + 1;
@@ -816,6 +1068,12 @@ export async function handleNotifyOpen(state, filePath, content, languageId, pre
816
1068
  // preserveDiagnostics: skip cache clear for format-only resyncs so
817
1069
  // waitForDiagnostics fast-paths instead of waiting up to 5s for TypeScript
818
1070
  // to re-publish what it already knows (formatting doesn't change semantics).
1071
+ // #1095 note: this also retains the prior content `binding`. Until the
1072
+ // server republishes for the new version, a binding read compares the OLD
1073
+ // content hash against the NEW disk bytes → boundToCurrentDisk `false` →
1074
+ // the consumer demotes to inconclusive. That's the SAFE (#533) direction (a
1075
+ // transient "unconfirmed", never a false-clean), self-healing on the next
1076
+ // publish — not a correctness hazard, unlike the reopen false-TRUE above.
819
1077
  if (!preserveDiagnostics) {
820
1078
  clearDiagnosticsForPath(state, normalizedPath);
821
1079
  }
@@ -827,19 +1085,32 @@ export async function handleNotifyOpen(state, filePath, content, languageId, pre
827
1085
  textDocument: { uri },
828
1086
  });
829
1087
  state.openDocuments.delete(normalizedPath);
830
- state.documentVersions.set(normalizedPath, 0);
1088
+ state.openDocumentUris?.delete(normalizedPath);
1089
+ // #1095 (P2-3): carry the version counter FORWARD across the
1090
+ // close+reopen instead of resetting to 0. LSP lets a didOpen use any
1091
+ // version, and reusing 0 for successive resyncs made the version
1092
+ // ambiguous — a late publish for an earlier resync's content echoed the
1093
+ // SAME 0 as the current send, so the superseded-push guard (0 < 0 is
1094
+ // false) accepted it and `recordBinding` bound STALE diagnostics to the
1095
+ // CURRENT content's fingerprint → an affirmative boundToCurrentDisk TRUE
1096
+ // for a stale view (worse than "unknown"). Monotonic versions make that
1097
+ // late echo strictly older → dropped by isSupersededPush → never bound.
1098
+ state.documentVersions.set(normalizedPath, version);
831
1099
  if (!isClientAlive(state))
832
1100
  return;
833
1101
  await safeSendNotification(state.connection, "textDocument/didOpen", {
834
- textDocument: { uri, languageId, version: 0, text: content },
1102
+ textDocument: { uri, languageId, version, text: content },
835
1103
  });
1104
+ recordSentContent(state, normalizedPath, version, content);
836
1105
  state.openDocuments.add(normalizedPath);
1106
+ state.openDocumentUris?.set(normalizedPath, uri);
837
1107
  return;
838
1108
  }
839
1109
  await safeSendNotification(state.connection, "textDocument/didChange", {
840
1110
  textDocument: { uri, version },
841
1111
  contentChanges: [{ text: content }],
842
1112
  });
1113
+ recordSentContent(state, normalizedPath, version, content);
843
1114
  return;
844
1115
  }
845
1116
  state.pendingOpens.add(normalizedPath);
@@ -873,14 +1144,17 @@ export async function handleNotifyOpen(state, filePath, content, languageId, pre
873
1144
  await safeSendNotification(state.connection, "textDocument/didOpen", {
874
1145
  textDocument: { uri, languageId, version: 0, text: content },
875
1146
  });
1147
+ recordSentContent(state, normalizedPath, 0, content);
876
1148
  state.pendingOpens.delete(normalizedPath);
877
1149
  state.openDocuments.add(normalizedPath);
1150
+ state.closedDocuments?.delete(normalizedPath);
1151
+ state.openDocumentUris?.set(normalizedPath, uri);
878
1152
  }
879
1153
  export async function handleNotifyChange(state, filePath, content) {
880
1154
  if (!isClientAlive(state))
881
1155
  return;
882
- const uri = pathToFileURL(filePath).href;
883
1156
  const normalizedPath = normalizeMapKey(filePath);
1157
+ const uri = state.openDocumentUris?.get(normalizedPath) ?? pathToFileURL(filePath).href;
884
1158
  if (!state.openDocuments.has(normalizedPath)) {
885
1159
  // Safety fallback: keep protocol ordering valid even if caller sends
886
1160
  // didChange before first didOpen for this document.
@@ -888,7 +1162,9 @@ export async function handleNotifyChange(state, filePath, content) {
888
1162
  textDocument: { uri, languageId: "plaintext", version: 0, text: content },
889
1163
  });
890
1164
  state.documentVersions.set(normalizedPath, 0);
1165
+ recordSentContent(state, normalizedPath, 0, content);
891
1166
  state.openDocuments.add(normalizedPath);
1167
+ state.openDocumentUris?.set(normalizedPath, uri);
892
1168
  return;
893
1169
  }
894
1170
  const version = (state.documentVersions.get(normalizedPath) ?? 0) + 1;
@@ -900,6 +1176,25 @@ export async function handleNotifyChange(state, filePath, content) {
900
1176
  textDocument: { uri, version },
901
1177
  contentChanges: [{ text: content }],
902
1178
  });
1179
+ recordSentContent(state, normalizedPath, version, content);
1180
+ }
1181
+ /** Close a document through the same lifecycle path exposed by the client. */
1182
+ export async function closeDocument(state, filePath) {
1183
+ if (!isClientAlive(state))
1184
+ return;
1185
+ const normalizedPath = normalizeMapKey(filePath);
1186
+ if (!state.openDocuments.has(normalizedPath))
1187
+ return;
1188
+ await safeSendNotification(state.connection, "textDocument/didClose", {
1189
+ textDocument: {
1190
+ uri: state.openDocumentUris?.get(normalizedPath) ?? pathToFileURL(filePath).href,
1191
+ },
1192
+ });
1193
+ state.openDocuments.delete(normalizedPath);
1194
+ state.closedDocuments?.add(normalizedPath);
1195
+ state.openDocumentUris?.delete(normalizedPath);
1196
+ state.documentVersions.delete(normalizedPath);
1197
+ clearDiagnosticsForPath(state, normalizedPath);
903
1198
  }
904
1199
  export async function clientShutdown(state, options = {}) {
905
1200
  const shutdownStart = Date.now();
@@ -912,6 +1207,7 @@ export async function clientShutdown(state, options = {}) {
912
1207
  state.pendingDiagnostics.clear();
913
1208
  state.pendingOpens.clear();
914
1209
  state.openDocuments.clear();
1210
+ state.openDocumentUris?.clear();
915
1211
  // #271: drop any pending watched-files batch + its timer (a dying client's
916
1212
  // queued FS changes are moot, and the timer must not outlive the connection).
917
1213
  state.watchQueue?.cancel();
@@ -1030,13 +1326,44 @@ signal = getAmbientAbortSignal()) {
1030
1326
  }
1031
1327
  return result;
1032
1328
  }
1329
+ // #1277: cheap liveness round-trip used by the silent-clean gates in
1330
+ // `index.ts`. `isAlive()`/`checkAlive()` only look at process/connection
1331
+ // state — a server that accepted the notify write and then wedged (still
1332
+ // running, connection still open, just never replying) reads as "alive" by
1333
+ // those checks even though it will never answer anything again. This sends a
1334
+ // real request (`workspace/symbol`, chosen because it needs no open document)
1335
+ // and reports whether the connection round-tripped it — success, a genuine
1336
+ // protocol-level error (e.g. MethodNotFound), and a stream-destroyed/
1337
+ // cancelled response (safeSendRequest swallows those to `undefined`, so the
1338
+ // final `isClientAlive` re-check is what catches "died mid-flight") ALL count
1339
+ // as "alive"; only a real timeout, or the connection having gone down by the
1340
+ // time this resolves, reports dead. The response content itself is never
1341
+ // inspected — only whether one arrived in time.
1342
+ async function clientPingLiveness(state, timeoutMs = LIVENESS_PING_TIMEOUT_MS) {
1343
+ if (!isClientAlive(state))
1344
+ return false;
1345
+ try {
1346
+ await withTimeout(safeSendRequest(state.connection, "workspace/symbol", {
1347
+ query: LIVENESS_PING_QUERY,
1348
+ }), timeoutMs);
1349
+ }
1350
+ catch (err) {
1351
+ if (err instanceof Error && err.message.startsWith("Timeout after")) {
1352
+ return false;
1353
+ }
1354
+ // A real protocol-level error reply still proves the server round-
1355
+ // tripped the request — fall through to the alive re-check below
1356
+ // rather than treating an error response as "dead".
1357
+ }
1358
+ return isClientAlive(state);
1359
+ }
1033
1360
  // Run an advertised server command via workspace/executeCommand, with the
1034
1361
  // generous EXECUTE_COMMAND_TIMEOUT_MS anti-deadlock backstop. Preserves the
1035
1362
  // hardening invariants: allowlist-by-advertisement (only commands the server
1036
1363
  // declared) and the serverEditsAllowed window that gates server-driven
1037
1364
  // applyEdit to the duration of an explicit call. Exported with an overridable
1038
1365
  // `timeoutMs` for the #365 regression tests.
1039
- export async function runServerCommand(state, command, args, timeoutMs = EXECUTE_COMMAND_TIMEOUT_MS) {
1366
+ export async function runServerCommand(state, command, args, timeoutMs = EXECUTE_COMMAND_TIMEOUT_MS, mutationContext) {
1040
1367
  if (!isClientAlive(state)) {
1041
1368
  return { executed: false, reason: "lsp client not alive" };
1042
1369
  }
@@ -1047,6 +1374,11 @@ export async function runServerCommand(state, command, args, timeoutMs = EXECUTE
1047
1374
  };
1048
1375
  }
1049
1376
  state.serverEditsAllowed += 1;
1377
+ state.activeMutationDepth = (state.activeMutationDepth ?? 0) + 1;
1378
+ if (state.activeMutationDepth === 1)
1379
+ state.activeMutationContext = mutationContext;
1380
+ else
1381
+ state.activeMutationContext = undefined;
1050
1382
  try {
1051
1383
  let result;
1052
1384
  try {
@@ -1072,22 +1404,85 @@ export async function runServerCommand(state, command, args, timeoutMs = EXECUTE
1072
1404
  }
1073
1405
  finally {
1074
1406
  state.serverEditsAllowed -= 1;
1407
+ state.activeMutationDepth = Math.max(0, (state.activeMutationDepth ?? 0) - 1);
1408
+ if (state.activeMutationDepth === 0)
1409
+ state.activeMutationContext = undefined;
1075
1410
  }
1076
1411
  }
1412
+ function validateWorkspaceEditVersions(state, edit) {
1413
+ for (const change of edit.documentChanges ?? []) {
1414
+ if (typeof change !== "object" || change === null || !("textDocument" in change))
1415
+ continue;
1416
+ const textDocument = change.textDocument;
1417
+ if (!textDocument || typeof textDocument.uri !== "string" || textDocument.version == null)
1418
+ continue;
1419
+ const current = state.documentVersions.get(normalizeMapKey(uriToPath(textDocument.uri)));
1420
+ if (current === undefined || current !== textDocument.version) {
1421
+ throw new Error(`stale workspace edit document version for ${textDocument.uri}`);
1422
+ }
1423
+ }
1424
+ }
1425
+ // Neutralize numeric `textDocument.version` stamps AFTER they have been
1426
+ // validated against the live document map. The tool apply paths (rename
1427
+ // apply:true in tools/lsp-navigation.ts, code-action autofix in
1428
+ // actionable-warnings.ts) call applyWorkspaceEdit without a documentVersions
1429
+ // map, so a preserved numeric version would fail preflight 100% of the time for
1430
+ // servers that stamp real versions (gopls stamps open documents). Setting the
1431
+ // version to null is the spec's "do not check" — the freshness guarantee has
1432
+ // already been provided here by validateWorkspaceEditVersions at the correct
1433
+ // moment. The server-initiated workspace/applyEdit handler does NOT route
1434
+ // through here (it applies params.edit directly with state.documentVersions),
1435
+ // so its real preflight version check is left fully intact.
1436
+ function stripDocumentVersions(edit) {
1437
+ if (!Array.isArray(edit.documentChanges))
1438
+ return edit;
1439
+ const documentChanges = edit.documentChanges.map((change) => {
1440
+ if (typeof change === "object" &&
1441
+ change !== null &&
1442
+ "textDocument" in change &&
1443
+ "edits" in change) {
1444
+ const textDocument = change
1445
+ .textDocument;
1446
+ if (textDocument && typeof textDocument.version === "number") {
1447
+ return {
1448
+ ...change,
1449
+ textDocument: { ...textDocument, version: null },
1450
+ };
1451
+ }
1452
+ }
1453
+ return change;
1454
+ });
1455
+ return { ...edit, documentChanges };
1456
+ }
1457
+ export async function normalizeClientWorkspaceEdit(state, edit) {
1458
+ validateWorkspaceEditVersions(state, edit);
1459
+ const normalized = (await normalizeWorkspaceEditToUtf16(edit, state.positionEncoding, state.root));
1460
+ return stripDocumentVersions(normalized);
1461
+ }
1077
1462
  async function resolveCodeActionBestEffort(state, action) {
1078
- if (!isClientAlive(state) || action.edit)
1463
+ if (!isClientAlive(state))
1079
1464
  return action;
1465
+ if (action.edit) {
1466
+ return {
1467
+ ...action,
1468
+ edit: await normalizeClientWorkspaceEdit(state, action.edit),
1469
+ };
1470
+ }
1471
+ let resolved;
1080
1472
  try {
1081
- const resolved = await withTimeout(safeSendRequest(state.connection, "codeAction/resolve", action), NAV_REQUEST_TIMEOUT_MS);
1082
- if (!resolved || typeof resolved !== "object")
1083
- return action;
1084
- return { ...action, ...resolved };
1473
+ resolved = await withTimeout(safeSendRequest(state.connection, "codeAction/resolve", action), NAV_REQUEST_TIMEOUT_MS);
1085
1474
  }
1086
1475
  catch {
1087
1476
  // codeAction/resolve is optional. Keep the original lightweight action when
1088
1477
  // the server does not support resolve or fails to populate an edit.
1089
1478
  return action;
1090
1479
  }
1480
+ if (!resolved || typeof resolved !== "object")
1481
+ return action;
1482
+ const merged = { ...action, ...resolved };
1483
+ return merged.edit
1484
+ ? { ...merged, edit: await normalizeClientWorkspaceEdit(state, merged.edit) }
1485
+ : merged;
1091
1486
  }
1092
1487
  // --- Client Factory ---
1093
1488
  export async function createLSPClient(options) {
@@ -1187,6 +1582,7 @@ export async function createLSPClient(options) {
1187
1582
  isConnected: true,
1188
1583
  isDestroyed: false,
1189
1584
  shutdownRequested: false,
1585
+ exitedAt: undefined,
1190
1586
  connectionDisposed: false,
1191
1587
  lastError: undefined,
1192
1588
  connection,
@@ -1194,12 +1590,19 @@ export async function createLSPClient(options) {
1194
1590
  pushDiagnosticTimestamps: new Map(),
1195
1591
  documentPullDiagnostics: new Map(),
1196
1592
  documentPullDiagnosticTimestamps: new Map(),
1593
+ pullFailureHistory: [],
1197
1594
  pendingDiagnostics: new Map(),
1198
1595
  diagnosticEmitter,
1199
1596
  diagnosticsVersion: 0,
1200
1597
  documentVersions: new Map(),
1201
1598
  diagnosticDocVersions: new Map(),
1599
+ documentContentHashes: new Map(),
1600
+ diagnosticBindings: new Map(),
1601
+ pullResultIds: new Map(),
1602
+ workspacePullResultCache: new Map(),
1202
1603
  openDocuments: new Set(),
1604
+ closedDocuments: new Set(),
1605
+ openDocumentUris: new Map(),
1203
1606
  pendingOpens: new Set(),
1204
1607
  // these are filled in after initialize — cast to avoid two-phase init
1205
1608
  workspaceDiagnosticsSupport: undefined,
@@ -1209,6 +1612,7 @@ export async function createLSPClient(options) {
1209
1612
  dynamicRegistrations: new Map(),
1210
1613
  advertisedCommands: new Set(),
1211
1614
  serverEditsAllowed: 0,
1615
+ activeMutationDepth: 0,
1212
1616
  serverId,
1213
1617
  launchVariant,
1214
1618
  root,
@@ -1258,7 +1662,22 @@ export async function createLSPClient(options) {
1258
1662
  });
1259
1663
  });
1260
1664
  setTimeout(() => {
1261
- if (!lspProcess.process.killed && process.platform !== "win32") {
1665
+ // #1114: gate on the process's own observed `exitCode`/`signalCode`,
1666
+ // not `.killed` — `killProcessTree` above signals the POSIX process
1667
+ // GROUP via the raw `process.kill(-pid, …)`, which never touches
1668
+ // this `ChildProcess` instance's `.killed` flag, so `!…killed` here
1669
+ // was always true and this 2s backstop unconditionally re-sent
1670
+ // SIGKILL even when the group had already exited. `exitCode` alone
1671
+ // is insufficient too: a process that died FROM a signal (the
1672
+ // common case here — killProcessTree's own SIGTERM/SIGKILL) has
1673
+ // `exitCode === null` forever and only `signalCode` set, so
1674
+ // checking `exitCode === null` alone still re-SIGKILLs that corpse
1675
+ // on the common path (harmless — `kill()` on an already-exited pid
1676
+ // is a swallowed no-op — but not actually "observed still alive").
1677
+ // Require both null to mean "no exit observed by either signal".
1678
+ if (lspProcess.process.exitCode === null &&
1679
+ lspProcess.process.signalCode === null &&
1680
+ process.platform !== "win32") {
1262
1681
  lspProcess.process.kill("SIGKILL");
1263
1682
  }
1264
1683
  }, 2000);
@@ -1312,10 +1731,20 @@ export async function createLSPClient(options) {
1312
1731
  /** True if the server process has exited or been killed. */
1313
1732
  processExited: () => lspProcess.process.exitCode !== null ||
1314
1733
  lspProcess.process.killed === true,
1734
+ /** #1127: mirrors `state.shutdownRequested` — see interface doc. */
1735
+ wasShutdownIntentional: () => state.shutdownRequested,
1736
+ /** #1127: mirrors `state.exitedAt` — see interface doc. */
1737
+ getExitedAt: () => state.exitedAt,
1315
1738
  /** Last N lines of server stderr for diagnostics. */
1316
1739
  recentStderr: (lines) => recentStderr(lines),
1740
+ getPullFailureHistory: () => state.pullFailureHistory.map((entry) => ({
1741
+ ...entry,
1742
+ message: entry.message.slice(0, 200),
1743
+ })),
1317
1744
  /** Pre-request health check — returns error string if dead. */
1318
1745
  checkAlive: () => checkProcessAlive(),
1746
+ /** #1277: cheap request round-trip proving the server still responds. */
1747
+ pingLiveness: (timeoutMs) => clientPingLiveness(state, timeoutMs),
1319
1748
  notify: {
1320
1749
  async open(filePath, content, languageId, preserveDiagnostics, silent) {
1321
1750
  return handleNotifyOpen(state, filePath, content, languageId, preserveDiagnostics, silent);
@@ -1327,6 +1756,9 @@ export async function createLSPClient(options) {
1327
1756
  getDiagnostics(filePath) {
1328
1757
  return getMergedDiagnosticsForPath(state, normalizeMapKey(filePath));
1329
1758
  },
1759
+ getDiagnosticBinding(filePath) {
1760
+ return state.diagnosticBindings.get(normalizeMapKey(filePath));
1761
+ },
1330
1762
  getAllDiagnostics() {
1331
1763
  const result = new Map();
1332
1764
  const keys = new Set([
@@ -1337,17 +1769,16 @@ export async function createLSPClient(options) {
1337
1769
  result.set(key, {
1338
1770
  diags: getMergedDiagnosticsForPath(state, key),
1339
1771
  ts: Math.max(state.pushDiagnosticTimestamps.get(key) ?? 0, state.documentPullDiagnosticTimestamps.get(key) ?? 0),
1772
+ binding: state.diagnosticBindings.get(key),
1340
1773
  });
1341
1774
  }
1342
1775
  return result;
1343
1776
  },
1344
1777
  getTrackedDiagnosticPaths() {
1345
- return [
1346
- ...new Set([
1778
+ return [...new Set([
1347
1779
  ...state.pushDiagnostics.keys(),
1348
1780
  ...state.documentPullDiagnostics.keys(),
1349
- ]),
1350
- ];
1781
+ ])].map((filePath) => process.platform === "win32" ? filePath.replace(/\//g, "\\") : filePath);
1351
1782
  },
1352
1783
  pruneDiagnostics(predicate) {
1353
1784
  let removed = 0;
@@ -1383,8 +1814,8 @@ export async function createLSPClient(options) {
1383
1814
  getLaunchVariant() {
1384
1815
  return state.launchVariant;
1385
1816
  },
1386
- async executeCommand(command, args) {
1387
- return runServerCommand(state, command, args);
1817
+ async executeCommand(command, args, mutationContext) {
1818
+ return runServerCommand(state, command, args, EXECUTE_COMMAND_TIMEOUT_MS, mutationContext);
1388
1819
  },
1389
1820
  get diagnosticsVersion() {
1390
1821
  return state.diagnosticsVersion;
@@ -1448,6 +1879,12 @@ export async function createLSPClient(options) {
1448
1879
  isDocumentOpen(filePath) {
1449
1880
  return state.openDocuments.has(normalizeMapKey(filePath));
1450
1881
  },
1882
+ isBusy() {
1883
+ return (activeRequestsByConnection.get(connection) ?? 0) > 0;
1884
+ },
1885
+ getDocumentUri(filePath) {
1886
+ return state.openDocumentUris?.get(normalizeMapKey(filePath));
1887
+ },
1451
1888
  async workspaceSymbol(query) {
1452
1889
  if (!isClientAlive(state))
1453
1890
  return [];
@@ -1487,8 +1924,9 @@ export async function createLSPClient(options) {
1487
1924
  position: await toWirePosition(state, filePath, line, character),
1488
1925
  newName,
1489
1926
  }, filePath);
1490
- return result ?? null;
1927
+ return result ? await normalizeClientWorkspaceEdit(state, result) : null;
1491
1928
  },
1929
+ closeDocument: (filePath) => closeDocument(state, filePath),
1492
1930
  async willRenameFiles(oldFilePath, newFilePath) {
1493
1931
  const result = await navRequest(state, "workspace/willRenameFiles", {
1494
1932
  files: [
@@ -1498,16 +1936,16 @@ export async function createLSPClient(options) {
1498
1936
  },
1499
1937
  ],
1500
1938
  });
1501
- return result ?? null;
1939
+ return result ? await normalizeClientWorkspaceEdit(state, result) : null;
1502
1940
  },
1503
- async didRenameFiles(oldFilePath, newFilePath) {
1941
+ async didRenameFiles(oldFilePath, newFilePath, oldUri, newUri) {
1504
1942
  if (!isClientAlive(state))
1505
1943
  return;
1506
1944
  await safeSendNotification(state.connection, "workspace/didRenameFiles", {
1507
1945
  files: [
1508
1946
  {
1509
- oldUri: pathToFileURL(oldFilePath).href,
1510
- newUri: pathToFileURL(newFilePath).href,
1947
+ oldUri: oldUri ?? pathToFileURL(oldFilePath).href,
1948
+ newUri: newUri ?? pathToFileURL(newFilePath).href,
1511
1949
  },
1512
1950
  ],
1513
1951
  });
@@ -1556,6 +1994,7 @@ async function safeSendNotification(connection, method, params) {
1556
1994
  throw err;
1557
1995
  }
1558
1996
  }
1997
+ const activeRequestsByConnection = new WeakMap();
1559
1998
  // Helper to safely send requests - catches stream destruction
1560
1999
  async function safeSendRequest(connection, method, params,
1561
2000
  // When provided, aborting the signal cancels the in-flight request via
@@ -1578,6 +2017,7 @@ signal) {
1578
2017
  const send = () => tokenSource
1579
2018
  ? connection.sendRequest(method, params, tokenSource.token)
1580
2019
  : connection.sendRequest(method, params);
2020
+ activeRequestsByConnection.set(connection, (activeRequestsByConnection.get(connection) ?? 0) + 1);
1581
2021
  try {
1582
2022
  // One safe retry on ContentModified (-32801): the document changed under
1583
2023
  // us, so the server discarded the request. A single retry beats returning
@@ -1607,6 +2047,11 @@ signal) {
1607
2047
  }
1608
2048
  }
1609
2049
  finally {
2050
+ const remaining = (activeRequestsByConnection.get(connection) ?? 1) - 1;
2051
+ if (remaining > 0)
2052
+ activeRequestsByConnection.set(connection, remaining);
2053
+ else
2054
+ activeRequestsByConnection.delete(connection);
1610
2055
  if (signal && onAbort)
1611
2056
  signal.removeEventListener("abort", onAbort);
1612
2057
  tokenSource?.dispose();