@herbertgao/pi-extensions 2026.8.3 → 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 (408) hide show
  1. package/README.md +45 -4
  2. package/examples/pi-footer.json +275 -0
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +5 -8
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  14. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  15. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  16. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +15 -2
  21. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  22. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  25. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  26. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  27. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  31. package/node_modules/@narumitw/pi-btw/README.md +8 -7
  32. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  33. package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
  34. package/node_modules/pi-footer/CHANGELOG.md +145 -0
  35. package/node_modules/pi-footer/LICENSE +21 -0
  36. package/node_modules/pi-footer/README.md +354 -0
  37. package/node_modules/pi-footer/package.json +68 -0
  38. package/node_modules/pi-footer/src/cache.ts +106 -0
  39. package/node_modules/pi-footer/src/colors.ts +237 -0
  40. package/node_modules/pi-footer/src/config.ts +202 -0
  41. package/node_modules/pi-footer/src/event-widgets.ts +41 -0
  42. package/node_modules/pi-footer/src/extension-statuses.ts +102 -0
  43. package/node_modules/pi-footer/src/git.ts +135 -0
  44. package/node_modules/pi-footer/src/index.ts +268 -0
  45. package/node_modules/pi-footer/src/metrics.ts +141 -0
  46. package/node_modules/pi-footer/src/presets.ts +383 -0
  47. package/node_modules/pi-footer/src/render.ts +114 -0
  48. package/node_modules/pi-footer/src/separators.ts +52 -0
  49. package/node_modules/pi-footer/src/types.ts +122 -0
  50. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +22 -0
  51. package/node_modules/pi-footer/src/ui/color-options.ts +195 -0
  52. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +61 -0
  53. package/node_modules/pi-footer/src/ui/edit-colors.ts +13 -0
  54. package/node_modules/pi-footer/src/ui/events.ts +35 -0
  55. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +60 -0
  56. package/node_modules/pi-footer/src/ui/extension-statuses.ts +73 -0
  57. package/node_modules/pi-footer/src/ui/fields.ts +252 -0
  58. package/node_modules/pi-footer/src/ui/global-menu.ts +126 -0
  59. package/node_modules/pi-footer/src/ui/helpers.ts +44 -0
  60. package/node_modules/pi-footer/src/ui/layout.ts +17 -0
  61. package/node_modules/pi-footer/src/ui/line-list.ts +19 -0
  62. package/node_modules/pi-footer/src/ui/model.ts +64 -0
  63. package/node_modules/pi-footer/src/ui/navigation.ts +29 -0
  64. package/node_modules/pi-footer/src/ui/option-edit.ts +93 -0
  65. package/node_modules/pi-footer/src/ui/overlay-render.ts +103 -0
  66. package/node_modules/pi-footer/src/ui/screen-context.ts +18 -0
  67. package/node_modules/pi-footer/src/ui/screen-controller.ts +27 -0
  68. package/node_modules/pi-footer/src/ui/screen-render.ts +32 -0
  69. package/node_modules/pi-footer/src/ui/screen-state.ts +20 -0
  70. package/node_modules/pi-footer/src/ui/screen.ts +243 -0
  71. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +92 -0
  72. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +52 -0
  73. package/node_modules/pi-footer/src/ui/screens/controller.ts +12 -0
  74. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +68 -0
  75. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +115 -0
  76. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +61 -0
  77. package/node_modules/pi-footer/src/ui/screens/global.ts +75 -0
  78. package/node_modules/pi-footer/src/ui/screens/line-list.ts +108 -0
  79. package/node_modules/pi-footer/src/ui/screens/main.ts +99 -0
  80. package/node_modules/pi-footer/src/ui/screens/terminal.ts +121 -0
  81. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +154 -0
  82. package/node_modules/pi-footer/src/ui/terminal-menu.ts +32 -0
  83. package/node_modules/pi-footer/src/ui/theme.ts +48 -0
  84. package/node_modules/pi-footer/src/ui/title-bar.ts +100 -0
  85. package/node_modules/pi-footer/src/ui/widget-actions.ts +76 -0
  86. package/node_modules/pi-footer/src/ui.ts +66 -0
  87. package/node_modules/pi-footer/src/widgets/context.ts +28 -0
  88. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +17 -0
  89. package/node_modules/pi-footer/src/widgets/core/context-window.ts +24 -0
  90. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +19 -0
  91. package/node_modules/pi-footer/src/widgets/core/cwd.ts +108 -0
  92. package/node_modules/pi-footer/src/widgets/core/event.ts +34 -0
  93. package/node_modules/pi-footer/src/widgets/core/external-status.ts +102 -0
  94. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +20 -0
  95. package/node_modules/pi-footer/src/widgets/core/model.ts +35 -0
  96. package/node_modules/pi-footer/src/widgets/core/provider.ts +17 -0
  97. package/node_modules/pi-footer/src/widgets/core/session-name.ts +17 -0
  98. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +17 -0
  99. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +17 -0
  100. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +17 -0
  101. package/node_modules/pi-footer/src/widgets/git/branch.ts +81 -0
  102. package/node_modules/pi-footer/src/widgets/git/clean.ts +19 -0
  103. package/node_modules/pi-footer/src/widgets/git/deletions.ts +17 -0
  104. package/node_modules/pi-footer/src/widgets/git/diff.ts +38 -0
  105. package/node_modules/pi-footer/src/widgets/git/insertions.ts +17 -0
  106. package/node_modules/pi-footer/src/widgets/git/remote.ts +17 -0
  107. package/node_modules/pi-footer/src/widgets/git/root.ts +17 -0
  108. package/node_modules/pi-footer/src/widgets/git/sha.ts +17 -0
  109. package/node_modules/pi-footer/src/widgets/git/staged.ts +17 -0
  110. package/node_modules/pi-footer/src/widgets/git/status.ts +19 -0
  111. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +17 -0
  112. package/node_modules/pi-footer/src/widgets/git/untracked.ts +17 -0
  113. package/node_modules/pi-footer/src/widgets/instance.ts +118 -0
  114. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +18 -0
  115. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +32 -0
  116. package/node_modules/pi-footer/src/widgets/layout/separator.ts +57 -0
  117. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +31 -0
  118. package/node_modules/pi-footer/src/widgets/options.ts +122 -0
  119. package/node_modules/pi-footer/src/widgets/project/runtime.ts +361 -0
  120. package/node_modules/pi-footer/src/widgets/registry.ts +223 -0
  121. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +17 -0
  122. package/node_modules/pi-footer/src/widgets/session/compactions.ts +17 -0
  123. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +18 -0
  124. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +18 -0
  125. package/node_modules/pi-footer/src/widgets/session/messages.ts +19 -0
  126. package/node_modules/pi-footer/src/widgets/session/session-id.ts +17 -0
  127. package/node_modules/pi-footer/src/widgets/session/session-start.ts +18 -0
  128. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +17 -0
  129. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +19 -0
  130. package/node_modules/pi-footer/src/widgets/session/total-time.ts +18 -0
  131. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +17 -0
  132. package/node_modules/pi-footer/src/widgets/store.ts +26 -0
  133. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +55 -0
  134. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +18 -0
  135. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +18 -0
  136. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +108 -0
  137. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +26 -0
  138. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +29 -0
  139. package/node_modules/pi-footer/src/widgets/tokens/context.ts +29 -0
  140. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +51 -0
  141. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +25 -0
  142. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +18 -0
  143. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +25 -0
  144. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +18 -0
  145. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +20 -0
  146. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +25 -0
  147. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +18 -0
  148. package/node_modules/pi-footer/src/widgets/types.ts +181 -0
  149. package/node_modules/pi-footer/src/widgets/utils/colors.ts +11 -0
  150. package/node_modules/pi-footer/src/widgets/utils/context.ts +112 -0
  151. package/node_modules/pi-footer/src/widgets/utils/session.ts +19 -0
  152. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +78 -0
  153. package/node_modules/pi-lens/CHANGELOG.md +2440 -954
  154. package/node_modules/pi-lens/README.md +13 -0
  155. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  156. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  157. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  158. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  159. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  160. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  161. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  162. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  163. package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
  164. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  165. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  166. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  167. package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
  168. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  169. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  170. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  171. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  172. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  173. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  174. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  175. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  176. package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
  177. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  178. package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
  179. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  180. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  181. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  182. package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
  183. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
  184. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  185. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
  186. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  187. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  188. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  189. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  190. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  191. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
  192. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  193. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  194. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  195. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  196. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  197. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  198. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  199. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  200. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  201. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  202. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  203. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
  204. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  205. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  206. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  207. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  208. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  209. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  210. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  211. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  212. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  213. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  214. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  215. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  216. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
  217. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  218. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  219. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  220. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  221. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  222. package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
  223. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  224. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  225. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  226. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  227. package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
  228. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  229. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  230. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  231. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  232. package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
  233. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  234. package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
  235. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
  236. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  237. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  238. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  239. package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
  240. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  241. package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
  242. package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
  243. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  244. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  245. package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
  246. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  247. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  248. package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
  249. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  250. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  251. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
  252. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  253. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
  254. package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
  255. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  256. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
  257. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  258. package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
  259. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  260. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  261. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  262. package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
  263. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  264. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  265. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  266. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
  267. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  268. package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
  269. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  270. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  271. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  272. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  273. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  274. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  275. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  276. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  277. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  278. package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
  279. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  280. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  281. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  282. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  283. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  284. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  285. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  286. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  287. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  288. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  289. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  290. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  291. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  292. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  293. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  294. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  295. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  296. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  297. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  298. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  299. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  300. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  301. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  302. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
  303. package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
  304. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
  305. package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
  306. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  307. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
  308. package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
  309. package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
  310. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  311. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
  312. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  313. package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
  314. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  315. package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
  316. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  317. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  318. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  319. package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
  320. package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
  321. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  322. package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
  323. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  324. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  325. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  326. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  327. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  328. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  329. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  330. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  331. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  332. package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
  333. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  334. package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
  335. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  336. package/node_modules/pi-lens/dist/index.js +49361 -36888
  337. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  338. package/node_modules/pi-lens/dist/mcp/server.js +211 -44
  339. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  340. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
  341. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
  342. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  343. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  344. package/node_modules/pi-lens/docs/agent-guide.md +2 -2
  345. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  346. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  347. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  348. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
  349. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  350. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  351. package/node_modules/pi-lens/docs/fable.md +27 -0
  352. package/node_modules/pi-lens/docs/features.md +31 -2
  353. package/node_modules/pi-lens/docs/globalconfig.md +45 -1
  354. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  355. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  356. package/node_modules/pi-lens/docs/mcp.md +37 -4
  357. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  358. package/node_modules/pi-lens/docs/settings.md +12 -1
  359. package/node_modules/pi-lens/docs/tools.md +4 -1
  360. package/node_modules/pi-lens/docs/usage.md +18 -0
  361. package/node_modules/pi-lens/package.json +10 -10
  362. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  363. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  364. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  365. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  366. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  367. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  368. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  369. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  370. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  371. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  372. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  373. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  374. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
  375. package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
  376. package/node_modules/pi-mcp-adapter/README.md +34 -3
  377. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  378. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  379. package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
  380. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  381. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  382. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  383. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  384. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  385. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  386. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  387. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  388. package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
  389. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  390. package/node_modules/pi-mcp-adapter/package.json +3 -3
  391. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  392. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  393. package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
  394. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  395. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  396. package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
  397. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  398. package/node_modules/pi-mcp-adapter/types.ts +93 -27
  399. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  400. package/package.json +17 -13
  401. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  402. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  403. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  404. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  405. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  406. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  407. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  408. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -20,6 +20,7 @@ import { isAtOrAboveHomeDir, normalizeEphemeralMapKey } from "./path-utils.js";
20
20
  import { createDebounceScheduler, } from "./persist-debounce.js";
21
21
  import { getWordIndexMaxFilesDerived } from "./project-scale.js";
22
22
  import { logWordIndex } from "./word-index-logger.js";
23
+ import { createDeadline, forEachCooperatively, yieldIfOverBudget, } from "./cooperative-budget.js";
23
24
  /**
24
25
  * The single normalizer every word-index path key folds through (#1025). The
25
26
  * CHEAP form — slash-fold + win32-lowercase, NO filesystem I/O — because the
@@ -94,52 +95,112 @@ export function tokenizeLine(line) {
94
95
  }
95
96
  return tokens;
96
97
  }
98
+ const WORD_INDEX_BUILD_YIELD_BUDGET_MS = 8;
97
99
  /**
98
- * Build the inverted index from file contents. One posting per (token, file,
99
- * line) a token repeated on the same line counts once so term frequency is
100
- * "lines mentioning the token", a stable signal that doesn't over-weight a line
101
- * that repeats an identifier. Document length is the total indexed token count.
100
+ * A line at or above this length is treated as a yield checkpoint on its own.
101
+ * The per-50-lines cadence is a proxy for work that only holds on hand-written
102
+ * source: a minified/bundled single-line file up to {@link WORD_INDEX_MAX_BYTES}
103
+ * that the source filter did not exclude would otherwise get ZERO in-document
104
+ * yields — one unbroken tokenize+push burst far above the budget (#1197 review
105
+ * finding 4). Checking the clock after any long line makes the cooperativeness
106
+ * claim true per BYTE, not just per line count.
102
107
  */
108
+ const WORD_INDEX_LONG_LINE_YIELD_CHARS = 4096;
109
+ /**
110
+ * Shared cooperative time-slicer for every bulk word-index path (#1197).
111
+ *
112
+ * Time-based, not count-based: "yield every N items" is only a bound when the
113
+ * per-item cost is bounded too, and the incremental refresh's per-document cost
114
+ * grows with the corpus (each replacement filters shared posting arrays). The
115
+ * #1197 outage was exactly that shape — 239 stale documents held pi's event loop
116
+ * for seconds between two "every 100 files" checkpoints. Callers combine this
117
+ * The shared deadline is checked at every bounded work unit so abort latency
118
+ * and occupancy are governed by the same monotonic budget.
119
+ */
120
+ function createEmptyWordIndex(truncated) {
121
+ return {
122
+ postings: new Map(),
123
+ docLengths: new PathKeyedMap(wordIndexKey),
124
+ totalTokens: 0,
125
+ docCount: 0,
126
+ truncated,
127
+ forward: new PathKeyedMap(wordIndexKey),
128
+ fileMtimes: new PathKeyedMap(wordIndexKey),
129
+ fileSizes: new PathKeyedMap(wordIndexKey),
130
+ };
131
+ }
132
+ function indexWordLine(index, filePath, line, lineNumber, tokenLineCounts) {
133
+ const lineTokens = tokenizeLine(line);
134
+ const seenOnLine = new Set();
135
+ for (const token of lineTokens) {
136
+ if (seenOnLine.has(token))
137
+ continue;
138
+ seenOnLine.add(token);
139
+ const arr = index.postings.get(token);
140
+ if (arr)
141
+ arr.push({ file: filePath, line: lineNumber });
142
+ else
143
+ index.postings.set(token, [{ file: filePath, line: lineNumber }]);
144
+ tokenLineCounts.set(token, (tokenLineCounts.get(token) ?? 0) + 1);
145
+ }
146
+ return lineTokens.length;
147
+ }
148
+ function finishWordIndexDocument(index, doc, docLength, tokenLineCounts) {
149
+ index.docLengths.set(doc.path, docLength);
150
+ index.forward?.set(doc.path, tokenLineCounts);
151
+ index.fileMtimes.set(doc.path, doc.mtimeMs ?? 0);
152
+ index.fileSizes.set(doc.path, doc.size ?? Buffer.byteLength(doc.content, "utf-8"));
153
+ index.totalTokens += docLength;
154
+ index.docCount += 1;
155
+ }
156
+ function indexWordDocument(index, doc) {
157
+ const lines = doc.content.split(/\r?\n/);
158
+ const tokenLineCounts = new Map();
159
+ let docLength = 0;
160
+ for (let i = 0; i < lines.length; i += 1) {
161
+ docLength += indexWordLine(index, doc.path, lines[i], i + 1, tokenLineCounts);
162
+ }
163
+ finishWordIndexDocument(index, doc, docLength, tokenLineCounts);
164
+ }
103
165
  export function buildWordIndex(files) {
104
- const postings = new Map();
105
- const docLengths = new PathKeyedMap(wordIndexKey);
106
- const forward = new PathKeyedMap(wordIndexKey);
107
- const fileMtimes = new PathKeyedMap(wordIndexKey);
108
- let totalTokens = 0;
109
- for (const { path: filePath, content, mtimeMs } of files) {
110
- const lines = content.split(/\r?\n/);
111
- let docLength = 0;
166
+ const index = createEmptyWordIndex(files.truncated ?? false);
167
+ for (const doc of files)
168
+ indexWordDocument(index, doc);
169
+ return index;
170
+ }
171
+ /**
172
+ * Cooperative production builder for bulk/cold paths. It produces the exact
173
+ * same index as {@link buildWordIndex}, but time-slices both between documents
174
+ * and within a large document so startup warmup cannot monopolize pi's TUI
175
+ * event loop (#1197). The index is private until this resolves, so a superseded
176
+ * build never publishes a partial replacement.
177
+ */
178
+ export async function buildWordIndexAsync(files, shouldContinue = () => true) {
179
+ const index = createEmptyWordIndex(files.truncated ?? false);
180
+ const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
181
+ for (const doc of files) {
182
+ if (!shouldContinue())
183
+ throw new Error("word index build superseded");
184
+ const lines = doc.content.split(/\r?\n/);
112
185
  const tokenLineCounts = new Map();
186
+ let docLength = 0;
113
187
  for (let i = 0; i < lines.length; i += 1) {
114
- const lineTokens = tokenizeLine(lines[i]);
115
- docLength += lineTokens.length;
116
- const seenOnLine = new Set();
117
- for (const token of lineTokens) {
118
- if (seenOnLine.has(token))
119
- continue;
120
- seenOnLine.add(token);
121
- const arr = postings.get(token);
122
- if (arr)
123
- arr.push({ file: filePath, line: i + 1 });
124
- else
125
- postings.set(token, [{ file: filePath, line: i + 1 }]);
126
- tokenLineCounts.set(token, (tokenLineCounts.get(token) ?? 0) + 1);
188
+ const line = lines[i];
189
+ docLength += indexWordLine(index, doc.path, line, i + 1, tokenLineCounts);
190
+ if (line.length >= WORD_INDEX_LONG_LINE_YIELD_CHARS ||
191
+ deadline.expired()) {
192
+ await yieldIfOverBudget(deadline);
193
+ if (!shouldContinue())
194
+ throw new Error("word index build superseded");
127
195
  }
128
196
  }
129
- docLengths.set(filePath, docLength);
130
- forward.set(filePath, tokenLineCounts);
131
- fileMtimes.set(filePath, mtimeMs ?? 0);
132
- totalTokens += docLength;
197
+ finishWordIndexDocument(index, doc, docLength, tokenLineCounts);
198
+ if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
199
+ if (!shouldContinue())
200
+ throw new Error("word index build superseded");
201
+ }
133
202
  }
134
- return {
135
- postings,
136
- docLengths,
137
- totalTokens,
138
- docCount: files.length,
139
- truncated: files.truncated ?? false,
140
- forward,
141
- fileMtimes,
142
- };
203
+ return index;
143
204
  }
144
205
  /**
145
206
  * Remove `filePath`'s postings/docLength/forward entry from `index` in place,
@@ -175,6 +236,7 @@ export function removeWordIndexDocument(index, filePath) {
175
236
  index.docLengths.delete(filePath);
176
237
  index.forward.delete(filePath);
177
238
  index.fileMtimes.delete(filePath);
239
+ index.fileSizes.delete(filePath);
178
240
  index.totalTokens -= docLength;
179
241
  index.docCount = Math.max(0, index.docCount - 1);
180
242
  return true;
@@ -238,6 +300,117 @@ export function updateWordIndexDocument(index, doc) {
238
300
  // legal on-disk mtime (SOURCE_DATE_EPOCH=0, archive extraction) and would
239
301
  // collide, leaving such a file never re-tokenized (#958 review F2).
240
302
  index.fileMtimes.set(doc.path, -1);
303
+ // Size is likewise recorded for the #1105 mtime+size refresh gate. The -1
304
+ // mtime already forces a re-read next session, so this value only keeps the
305
+ // map dense (parallel to fileMtimes); store the real byte length so a
306
+ // deserialize→reserialize round-trip before any refresh carries a truthful
307
+ // size rather than a placeholder.
308
+ index.fileSizes.set(doc.path, Buffer.byteLength(doc.content, "utf-8"));
309
+ index.totalTokens += docLength;
310
+ index.docCount += 1;
311
+ return true;
312
+ }
313
+ async function stageWordIndexDocumentRemoval(index, filePath, shouldContinue) {
314
+ if (!index.forward)
315
+ return undefined;
316
+ const tokenLineCounts = index.forward.get(filePath);
317
+ if (!tokenLineCounts)
318
+ return undefined;
319
+ const removedKey = wordIndexKey(filePath);
320
+ const postings = new Map();
321
+ const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
322
+ for (const token of tokenLineCounts.keys()) {
323
+ if (!shouldContinue())
324
+ throw new Error("word index refresh superseded");
325
+ const arr = index.postings.get(token);
326
+ if (!arr)
327
+ continue;
328
+ const next = [];
329
+ for (const hit of arr) {
330
+ if (wordIndexKey(hit.file) !== removedKey)
331
+ next.push(hit);
332
+ if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
333
+ if (!shouldContinue())
334
+ throw new Error("word index refresh superseded");
335
+ }
336
+ }
337
+ postings.set(token, next.length > 0 ? next : undefined);
338
+ }
339
+ return { postings, docLength: index.docLengths.get(filePath) ?? 0 };
340
+ }
341
+ function commitWordIndexDocumentRemoval(index, filePath, staged) {
342
+ for (const [token, hits] of staged.postings) {
343
+ if (hits)
344
+ index.postings.set(token, hits);
345
+ else
346
+ index.postings.delete(token);
347
+ }
348
+ index.docLengths.delete(filePath);
349
+ index.forward?.delete(filePath);
350
+ index.fileMtimes.delete(filePath);
351
+ index.fileSizes.delete(filePath);
352
+ index.totalTokens -= staged.docLength;
353
+ index.docCount = Math.max(0, index.docCount - 1);
354
+ }
355
+ /** Cooperative, atomically-published removal used only by bulk refresh. */
356
+ export async function removeWordIndexDocumentAsync(index, filePath, shouldContinue = () => true) {
357
+ const staged = await stageWordIndexDocumentRemoval(index, filePath, shouldContinue);
358
+ if (!staged)
359
+ return false;
360
+ if (!shouldContinue())
361
+ throw new Error("word index refresh superseded");
362
+ commitWordIndexDocumentRemoval(index, filePath, staged);
363
+ return true;
364
+ }
365
+ /** Cooperative replacement whose old/new state is committed without an await. */
366
+ export async function updateWordIndexDocumentAsync(index, doc, shouldContinue = () => true) {
367
+ if (!index.forward)
368
+ return false;
369
+ const removal = index.forward.has(doc.path)
370
+ ? await stageWordIndexDocumentRemoval(index, doc.path, shouldContinue)
371
+ : undefined;
372
+ if (index.forward.has(doc.path) && !removal)
373
+ return false;
374
+ const perTokenHits = new Map();
375
+ let docLength = 0;
376
+ const lines = doc.content.split(/\r?\n/);
377
+ await forEachCooperatively(lines, (line, i) => {
378
+ const lineTokens = tokenizeLine(line);
379
+ docLength += lineTokens.length;
380
+ const seenOnLine = new Set();
381
+ for (const token of lineTokens) {
382
+ if (seenOnLine.has(token))
383
+ continue;
384
+ seenOnLine.add(token);
385
+ const arr = perTokenHits.get(token);
386
+ if (arr)
387
+ arr.push(i + 1);
388
+ else
389
+ perTokenHits.set(token, [i + 1]);
390
+ }
391
+ }, {
392
+ budgetMs: WORD_INDEX_BUILD_YIELD_BUDGET_MS,
393
+ shouldContinue,
394
+ abortMessage: "word index refresh superseded",
395
+ });
396
+ if (!shouldContinue())
397
+ throw new Error("word index refresh superseded");
398
+ if (removal)
399
+ commitWordIndexDocumentRemoval(index, doc.path, removal);
400
+ const tokenLineCounts = new Map();
401
+ for (const [token, lineNumbers] of perTokenHits) {
402
+ tokenLineCounts.set(token, lineNumbers.length);
403
+ const hits = lineNumbers.map((line) => ({ file: doc.path, line }));
404
+ const arr = index.postings.get(token);
405
+ if (arr)
406
+ arr.push(...hits);
407
+ else
408
+ index.postings.set(token, hits);
409
+ }
410
+ index.docLengths.set(doc.path, docLength);
411
+ index.forward.set(doc.path, tokenLineCounts);
412
+ index.fileMtimes.set(doc.path, -1);
413
+ index.fileSizes.set(doc.path, Buffer.byteLength(doc.content, "utf-8"));
241
414
  index.totalTokens += docLength;
242
415
  index.docCount += 1;
243
416
  return true;
@@ -262,7 +435,7 @@ export const WORD_INDEX_MAX_BYTES = 512 * 1024;
262
435
  * `shouldContinue` lets a session-scoped caller abort early (session
263
436
  * superseded) without this module knowing about RuntimeCoordinator.
264
437
  */
265
- export async function collectWordIndexDocs(root, shouldContinue = () => true) {
438
+ export async function collectWordIndexDocs(root, shouldContinue = () => true, preflightFiles) {
266
439
  const { collectSourceFilesAsync } = await import("./source-filter.js");
267
440
  // #747 hardening: pass the cap INTO the walk — without it,
268
441
  // `collectSourceFilesAsync` defaults to an unbounded traversal and the
@@ -272,29 +445,39 @@ export async function collectWordIndexDocs(root, shouldContinue = () => true) {
272
445
  // visited-entry budget (DEFAULT_MAX_SCAN_ENTRIES), so a mixed tree with few
273
446
  // source files among a huge pile of non-source files can't force a
274
447
  // full-tree walk either; an index over the truncated list is acceptable.
275
- const maxFiles = getWordIndexMaxFilesDerived(root);
448
+ const maxFiles = preflightFiles?.length ?? getWordIndexMaxFilesDerived(root);
276
449
  // #894 review: prioritize code kinds within the cap — with broadened
277
450
  // enumeration, thousands of data/doc files (locale JSON, fixtures, …)
278
451
  // ahead of the code dirs in walk order could exhaust `maxFiles` and evict
279
452
  // real source files from the index entirely (DOC_FILE_PENALTY can't
280
453
  // rescue a file that never made the slice).
281
- const files = await collectSourceFilesAsync(root, {
282
- maxFiles,
283
- prioritizeCodeKinds: true,
284
- });
285
- const truncated = files.length === maxFiles;
454
+ const files = preflightFiles
455
+ ? preflightFiles.map((file) => file.path)
456
+ : await collectSourceFilesAsync(root, {
457
+ maxFiles,
458
+ prioritizeCodeKinds: true,
459
+ });
460
+ const truncated = preflightFiles?.truncated ?? files.length === maxFiles;
286
461
  const docs = Object.assign([], { truncated, skipped: 0 });
287
462
  if (!shouldContinue())
288
463
  return docs;
289
- let processed = 0;
464
+ const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
290
465
  for (const file of files.slice(0, maxFiles)) {
466
+ if (!shouldContinue())
467
+ return docs;
291
468
  try {
469
+ // A preflight list saves the second full WALK, but its metadata may be
470
+ // stale by the time this rebuild reads the file. Re-stat each document at
471
+ // content-read time so the stored freshness stamp describes the bytes we
472
+ // actually index and the next incremental refresh cannot miss a change in
473
+ // the walk-to-read window (#1302).
292
474
  const stat = fs.statSync(file);
293
475
  if (stat.size <= WORD_INDEX_MAX_BYTES) {
294
476
  docs.push({
295
477
  path: file,
296
478
  content: fs.readFileSync(file, "utf-8"),
297
479
  mtimeMs: stat.mtimeMs,
480
+ size: stat.size,
298
481
  });
299
482
  }
300
483
  else {
@@ -308,8 +491,7 @@ export async function collectWordIndexDocs(root, shouldContinue = () => true) {
308
491
  // unreadable / vanished file — skip, but count it (see above).
309
492
  docs.skipped += 1;
310
493
  }
311
- if (++processed % 100 === 0) {
312
- await new Promise((resolve) => setImmediate(resolve));
494
+ if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
313
495
  if (!shouldContinue())
314
496
  return docs;
315
497
  }
@@ -317,14 +499,44 @@ export async function collectWordIndexDocs(root, shouldContinue = () => true) {
317
499
  return docs;
318
500
  }
319
501
  const WORD_INDEX_INCREMENTAL_CHURN_THRESHOLD = 0.3;
502
+ // A ratio alone misclassifies one stale file in a three-file project as dense.
503
+ // Below this absolute floor, per-document replacement is bounded and cheaper
504
+ // than a second full walk/read even when the percentage is high (#1197).
505
+ const WORD_INDEX_DENSE_REFRESH_MIN_DOCUMENTS = 32;
506
+ /**
507
+ * Cost of one file's stat+read+decode expressed in tokenizer-token units, so
508
+ * the work model below can compare a path that re-reads EVERY file (full
509
+ * rebuild) against one that re-reads only the stale files (incremental) in a
510
+ * single currency. One read ≈ 300 tokens at the ~1 µs/token measured below.
511
+ * Order-of-magnitude by design — it only has to keep a tiny-vocabulary corpus
512
+ * (where posting scans are nearly free) from being sent to a full rebuild that
513
+ * re-reads everything to save a handful of array filters.
514
+ */
515
+ const WORD_INDEX_FILE_READ_TOKEN_COST = 300;
516
+ /**
517
+ * Cost of scanning ONE posting-array element (the `wordIndexKey` compare in
518
+ * {@link removeWordIndexDocument}'s filter) relative to tokenizing one token,
519
+ * so both sides of the model below are denominated in tokens. Measured on this
520
+ * repository's own 2,062-document corpus: 213 ns per element scan against 979 ns
521
+ * per token — and the resulting predicted crossover (23 stale documents) lands
522
+ * on the directly measured one (~21, from 97.8 ms per document replacement
523
+ * against a 1,436 ms rebuild + re-read). The synthetic high-df corpus in the
524
+ * #1197 probe gives 0.09, so this is the conservative (incremental-favouring)
525
+ * end of the observed range.
526
+ */
527
+ const WORD_INDEX_POSTING_SCAN_TOKEN_COST = 0.2;
320
528
  /**
321
529
  * Refresh a serializer-v2 index from the current bounded source-file set.
322
- * The walk/stat pass is cheap; only stale/new documents are read and tokenized.
323
- * Throws when the index cannot be updated safely so callers can full-rebuild.
530
+ * The walk/stat pass is cheap; only sparse stale/new documents are read and
531
+ * tokenized. Expected dense/legacy states return `full-required` BEFORE any
532
+ * mutation; unexpected corruption or supersession still throws.
324
533
  */
325
534
  export async function refreshWordIndexIncrementally(index, root, shouldContinue = () => true) {
326
- if (!index.forward || !index.fileMtimes) {
327
- throw new Error("word index lacks incremental metadata");
535
+ if (!index.forward || !index.fileMtimes || !index.fileSizes) {
536
+ return {
537
+ mode: "full-required",
538
+ reason: "missing-incremental-metadata",
539
+ };
328
540
  }
329
541
  const { collectSourceFilesAsync } = await import("./source-filter.js");
330
542
  const maxFiles = getWordIndexMaxFilesDerived(root);
@@ -347,17 +559,33 @@ export async function refreshWordIndexIncrementally(index, root, shouldContinue
347
559
  // `wordIndexKey` keeps build/edit/refresh convergent; `current`'s value
348
560
  // retains the raw walk path for the stat/read and for the display key.
349
561
  const current = new Map();
562
+ // Check both supersession and the monotonic budget at every stat unit. Unlike
563
+ // the former modulus cadence, this remains bounded as per-file work changes.
564
+ const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
350
565
  for (const file of walked) {
566
+ if (!shouldContinue())
567
+ throw new Error("word index refresh superseded");
351
568
  try {
352
569
  const stat = fs.statSync(file);
353
570
  if (stat.size <= WORD_INDEX_MAX_BYTES) {
354
- current.set(wordIndexKey(file), { path: file, mtimeMs: stat.mtimeMs });
571
+ current.set(wordIndexKey(file), {
572
+ path: file,
573
+ mtimeMs: stat.mtimeMs,
574
+ size: stat.size,
575
+ });
355
576
  }
356
577
  }
357
578
  catch {
358
579
  // A file vanishing between walk and stat is simply absent.
359
580
  }
581
+ if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
582
+ if (!shouldContinue())
583
+ throw new Error("word index refresh superseded");
584
+ }
360
585
  }
586
+ const preflightFiles = Object.assign([...current.values()], {
587
+ truncated: walked.length === maxFiles,
588
+ });
361
589
  const oldSet = new Set([...index.docLengths.keys()].map(wordIndexKey));
362
590
  let changedSet = 0;
363
591
  for (const key of oldSet)
@@ -368,14 +596,85 @@ export async function refreshWordIndexIncrementally(index, root, shouldContinue
368
596
  changedSet++;
369
597
  const denominator = Math.max(oldSet.size, current.size, 1);
370
598
  if (changedSet / denominator > WORD_INDEX_INCREMENTAL_CHURN_THRESHOLD) {
371
- throw new Error("word index file-set churn exceeds incremental threshold");
599
+ return { mode: "full-required", reason: "file-set-churn", preflightFiles };
600
+ }
601
+ // Replacing one document filters every shared posting array for that
602
+ // document's tokens. That is excellent for a sparse edit, but repeating it
603
+ // for most of the corpus becomes effectively quadratic. Decide the dense
604
+ // transition BEFORE dropping or editing anything so the old index remains a
605
+ // valid fallback until a separately-built full replacement is ready (#1197).
606
+ let staleDocuments = 0;
607
+ for (const { path: file, mtimeMs, size } of current.values()) {
608
+ if (index.fileMtimes.get(file) !== mtimeMs ||
609
+ (index.fileSizes.get(file) ?? -1) !== size) {
610
+ staleDocuments += 1;
611
+ }
612
+ }
613
+ // Density alone is NOT a bound (#1197 review finding 1): the per-document cost
614
+ // GROWS with the corpus, so a stale set comfortably under any fixed ratio (or
615
+ // any fixed absolute ceiling) still costs orders of magnitude more than a
616
+ // rebuild on a big enough repository. 800 documents with 239 stale — 29.875%,
617
+ // just under the ratio — measured 90.6 s with a 39.6 s synchronous block
618
+ // against 1.3 s / 10.6 ms for a full build, and at the 6,000-file cap up to
619
+ // 1,799 stale documents would have stayed on that path.
620
+ //
621
+ // So compare estimated WORK, both sides denominated in tokenizer tokens:
622
+ // incremental ≈ stale x (docTokens x postingLength x scanCost + readCost)
623
+ // full ≈ totalTokens + corpusSize x readCost
624
+ // This is self-bounding in a way no constant is: the incremental path is taken
625
+ // only while its total estimated cost stays under ONE full rebuild, so the
626
+ // worst case is "about as expensive as rebuilding", whatever the corpus size.
627
+ //
628
+ // Both statistics come from cheap metadata passes — one over `postings`
629
+ // (distinct tokens, reading only `.length`) and one over `forward` (documents,
630
+ // reading only `.size`). Neither touches a posting ELEMENT, which is the work
631
+ // this decision exists to avoid.
632
+ let postingEntries = 0;
633
+ let weightedPostingEntries = 0;
634
+ for (const hits of index.postings.values()) {
635
+ postingEntries += hits.length;
636
+ weightedPostingEntries += hits.length * hits.length;
637
+ }
638
+ // The mean posting-array length weighted by OCCURRENCES, not the plain mean:
639
+ // a document's tokens are drawn from the frequency distribution, so the arrays
640
+ // a removal actually filters skew hard toward the high-document-frequency
641
+ // tokens. The unweighted mean (dominated by the long tail of df=1 tokens)
642
+ // underestimates the real cost by an order of magnitude.
643
+ const expectedPostingLength = postingEntries > 0 ? weightedPostingEntries / postingEntries : 0;
644
+ let distinctTokenEntries = 0;
645
+ for (const tokenLineCounts of index.forward.values()) {
646
+ distinctTokenEntries += tokenLineCounts.size;
647
+ }
648
+ const expectedDocumentTokens = index.docCount > 0 ? distinctTokenEntries / index.docCount : 0;
649
+ const estimatedIncrementalWork = staleDocuments *
650
+ (expectedDocumentTokens *
651
+ expectedPostingLength *
652
+ WORD_INDEX_POSTING_SCAN_TOKEN_COST +
653
+ WORD_INDEX_FILE_READ_TOKEN_COST);
654
+ const estimatedFullRebuildWork = index.totalTokens + current.size * WORD_INDEX_FILE_READ_TOKEN_COST;
655
+ // The document-count floor guards the RATIO test only — its documented job is
656
+ // that "one stale file in a three-file project" is 33% but not dense. It must
657
+ // NOT gate the work test: a floor is another constant, and letting one
658
+ // suppress the work comparison would reintroduce exactly the unbounded-on-the
659
+ // -other-axis shape (31 documents at 98 ms each on this repository's own
660
+ // corpus, and worse as the corpus grows). The work test needs no floor — on a
661
+ // small project a full rebuild is cheap, so it never fires there.
662
+ if ((staleDocuments >= WORD_INDEX_DENSE_REFRESH_MIN_DOCUMENTS &&
663
+ staleDocuments / Math.max(current.size, 1) >
664
+ WORD_INDEX_INCREMENTAL_CHURN_THRESHOLD) ||
665
+ estimatedIncrementalWork > estimatedFullRebuildWork) {
666
+ return {
667
+ mode: "full-required",
668
+ reason: "stale-document-churn",
669
+ preflightFiles,
670
+ };
372
671
  }
373
672
  let dropped = 0;
374
673
  for (const key of oldSet) {
375
674
  if (!current.has(key)) {
376
675
  // `key` is already folded; removeWordIndexDocument re-folds it
377
676
  // idempotently via the PathKeyedMap, so the drop hits the right entry.
378
- if (!removeWordIndexDocument(index, key)) {
677
+ if (!(await removeWordIndexDocumentAsync(index, key, shouldContinue))) {
379
678
  throw new Error(`failed to drop word-index document: ${key}`);
380
679
  }
381
680
  dropped++;
@@ -383,9 +682,16 @@ export async function refreshWordIndexIncrementally(index, root, shouldContinue
383
682
  }
384
683
  let refreshed = 0;
385
684
  let skipped = 0;
386
- let processed = 0;
387
- for (const { path: file, mtimeMs } of current.values()) {
388
- if (index.fileMtimes.get(file) !== mtimeMs) {
685
+ for (const { path: file, mtimeMs, size } of current.values()) {
686
+ // #1105: mtime-first, size-second freshness mtime alone is a stale
687
+ // signal (mtime-preserving content changes serve stale identifiers to
688
+ // symbol_search). Size is free (the stat above already read it) and
689
+ // catches every content change that alters byte length. Both come from the
690
+ // SAME stat, so a mismatch on EITHER axis re-reads. `?? -1` makes a file
691
+ // absent from the (possibly legacy, pre-#1105) size map always count as
692
+ // changed → one-time re-read that repopulates the size, the safe direction.
693
+ if (index.fileMtimes.get(file) !== mtimeMs ||
694
+ (index.fileSizes.get(file) ?? -1) !== size) {
389
695
  // A file the walk/stat pass saw can still fail to read here — a
390
696
  // transient exclusive lock (antivirus, an editor, a build step) or a
391
697
  // file that vanished in the interim. Match collectWordIndexDocs'
@@ -400,14 +706,22 @@ export async function refreshWordIndexIncrementally(index, root, shouldContinue
400
706
  skipped++;
401
707
  continue;
402
708
  }
403
- if (!updateWordIndexDocument(index, { path: file, content })) {
709
+ if (!(await updateWordIndexDocumentAsync(index, { path: file, content }, shouldContinue))) {
404
710
  throw new Error(`failed to refresh word-index document: ${file}`);
405
711
  }
712
+ // updateWordIndexDocument stamps mtime=-1 (per-edit convention); the
713
+ // refresh path KNOWS the real on-disk stat, so overwrite both axes with
714
+ // the true values so this document is not needlessly re-read next pass.
406
715
  index.fileMtimes.set(file, mtimeMs);
716
+ index.fileSizes.set(file, size);
407
717
  refreshed++;
408
718
  }
409
- if (++processed % 100 === 0) {
410
- await new Promise((resolve) => setImmediate(resolve));
719
+ // Same OR as the stat loop. This is the loop the #1197 outage actually
720
+ // blocked in: replacing one document costs more the larger the corpus is,
721
+ // so a count-only checkpoint let 100 replacements run back to back for
722
+ // seconds. The gate above keeps the stale set sparse; this keeps even a
723
+ // sparse-but-expensive set off the event loop.
724
+ if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
411
725
  if (!shouldContinue())
412
726
  throw new Error("word index refresh superseded");
413
727
  }
@@ -510,6 +824,8 @@ export function centralityFromReverseDeps(index, reverseDeps, normalizeKey = (fi
510
824
  }
511
825
  return centrality;
512
826
  }
827
+ /** Persisted word-index serialization format version. Bump on breaking format changes. */
828
+ export const WORD_INDEX_FORMAT_VERSION = 2;
513
829
  export function serializeWordIndex(index) {
514
830
  const files = [...index.docLengths.keys()];
515
831
  const fileIndex = new Map();
@@ -533,7 +849,7 @@ export function serializeWordIndex(index) {
533
849
  ])
534
850
  : undefined;
535
851
  return {
536
- version: 2,
852
+ version: WORD_INDEX_FORMAT_VERSION,
537
853
  files,
538
854
  postings,
539
855
  docLengths: files.map((file) => index.docLengths.get(file) ?? 0),
@@ -541,12 +857,13 @@ export function serializeWordIndex(index) {
541
857
  indexedFileCount: index.docCount,
542
858
  truncated: index.truncated,
543
859
  fileMtimes: files.map((file) => index.fileMtimes.get(file) ?? 0),
860
+ fileSizes: files.map((file) => index.fileSizes.get(file) ?? 0),
544
861
  forward,
545
862
  };
546
863
  }
547
864
  export function deserializeWordIndex(data) {
548
865
  if (!data ||
549
- data.version !== 2 ||
866
+ data.version !== WORD_INDEX_FORMAT_VERSION ||
550
867
  !Array.isArray(data.files) ||
551
868
  !Array.isArray(data.postings) ||
552
869
  !Array.isArray(data.docLengths) ||
@@ -556,8 +873,17 @@ export function deserializeWordIndex(data) {
556
873
  }
557
874
  const docLengths = new PathKeyedMap(wordIndexKey);
558
875
  const fileMtimes = new PathKeyedMap(wordIndexKey);
876
+ const fileSizes = new PathKeyedMap(wordIndexKey);
559
877
  data.files.forEach((file, i) => docLengths.set(file, data.docLengths[i] ?? 0));
560
878
  data.files.forEach((file, i) => fileMtimes.set(file, data.fileMtimes[i] ?? 0));
879
+ // #1105: `fileSizes` is optional on the wire (pre-#1105 snapshots omit it).
880
+ // Only populate when the array is present AND parallel to `files`; otherwise
881
+ // leave it empty so the refresh gate re-reads every file once to repopulate,
882
+ // rather than trusting a bogus/misaligned size. Never treated as "current".
883
+ if (Array.isArray(data.fileSizes) &&
884
+ data.fileSizes.length === data.files.length) {
885
+ data.files.forEach((file, i) => fileSizes.set(file, data.fileSizes?.[i] ?? 0));
886
+ }
561
887
  const postings = new Map();
562
888
  for (const [token, flat] of data.postings) {
563
889
  if (typeof token !== "string" || !Array.isArray(flat))
@@ -603,6 +929,7 @@ export function deserializeWordIndex(data) {
603
929
  truncated: data.truncated === true,
604
930
  forward,
605
931
  fileMtimes,
932
+ fileSizes,
606
933
  };
607
934
  }
608
935
  const buildStatuses = new Map();
@@ -652,7 +979,7 @@ export function triggerBackgroundWordIndexBuild(cwd, dbg, options = {}) {
652
979
  try {
653
980
  const { loadProjectSnapshot, saveProjectSnapshot, PROJECT_SNAPSHOT_VERSION } = await import("./project-snapshot.js");
654
981
  const docs = await collectWordIndexDocs(key);
655
- const index = buildWordIndex(docs);
982
+ const index = await buildWordIndexAsync(docs);
656
983
  const existing = loadProjectSnapshot(key);
657
984
  const snapshot = existing ?? {
658
985
  version: PROJECT_SNAPSHOT_VERSION,