@oh-my-pi/pi-coding-agent 15.10.9 → 15.10.11

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 (352) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/dist/cli.js +23087 -0
  3. package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
  4. package/dist/types/async/job-manager.d.ts +18 -0
  5. package/dist/types/cli/args.d.ts +1 -1
  6. package/dist/types/cli/dry-balance-cli.d.ts +1 -1
  7. package/dist/types/cli/gallery-cli.d.ts +1 -1
  8. package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
  9. package/dist/types/cli/usage-cli.d.ts +72 -0
  10. package/dist/types/commands/launch.d.ts +1 -1
  11. package/dist/types/commands/read.d.ts +1 -1
  12. package/dist/types/commands/usage.d.ts +25 -0
  13. package/dist/types/config/append-only-context-mode.d.ts +2 -1
  14. package/dist/types/config/model-discovery.d.ts +55 -0
  15. package/dist/types/config/model-registry.d.ts +20 -219
  16. package/dist/types/config/model-resolver.d.ts +16 -10
  17. package/dist/types/config/model-roles.d.ts +28 -0
  18. package/dist/types/config/models-config-schema.d.ts +523 -42
  19. package/dist/types/config/models-config.d.ts +385 -0
  20. package/dist/types/config/settings-schema.d.ts +12 -16
  21. package/dist/types/config/settings.d.ts +1 -1
  22. package/dist/types/debug/log-viewer.d.ts +1 -1
  23. package/dist/types/debug/raw-sse.d.ts +1 -1
  24. package/dist/types/debug/terminal-info.d.ts +0 -1
  25. package/dist/types/eval/backend.d.ts +0 -2
  26. package/dist/types/eval/idle-timeout.d.ts +0 -4
  27. package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
  28. package/dist/types/export/html/template.generated.d.ts +1 -1
  29. package/dist/types/extensibility/extensions/types.d.ts +3 -3
  30. package/dist/types/hindsight/mental-models.d.ts +17 -8
  31. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  32. package/dist/types/internal-urls/types.d.ts +1 -1
  33. package/dist/types/lsp/edits.d.ts +9 -0
  34. package/dist/types/lsp/index.d.ts +2 -2
  35. package/dist/types/lsp/types.d.ts +2 -0
  36. package/dist/types/lsp/utils.d.ts +3 -0
  37. package/dist/types/mcp/json-rpc.d.ts +5 -0
  38. package/dist/types/mnemopi/state.d.ts +11 -1
  39. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  40. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  41. package/dist/types/modes/components/bash-execution.d.ts +1 -1
  42. package/dist/types/modes/components/copy-selector.d.ts +1 -1
  43. package/dist/types/modes/components/dynamic-border.d.ts +1 -1
  44. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
  45. package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
  46. package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
  47. package/dist/types/modes/components/footer.d.ts +1 -1
  48. package/dist/types/modes/components/hook-editor.d.ts +5 -0
  49. package/dist/types/modes/components/hook-input.d.ts +4 -0
  50. package/dist/types/modes/components/hook-selector.d.ts +1 -1
  51. package/dist/types/modes/components/model-selector.d.ts +1 -1
  52. package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
  53. package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
  54. package/dist/types/modes/components/session-selector.d.ts +1 -1
  55. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  56. package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
  57. package/dist/types/modes/components/transcript-container.d.ts +31 -26
  58. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  59. package/dist/types/modes/components/user-message-selector.d.ts +1 -1
  60. package/dist/types/modes/components/user-message.d.ts +2 -1
  61. package/dist/types/modes/components/visual-truncate.d.ts +1 -1
  62. package/dist/types/modes/components/welcome.d.ts +19 -3
  63. package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
  64. package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
  65. package/dist/types/modes/interactive-mode.d.ts +1 -1
  66. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  67. package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
  68. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  69. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
  70. package/dist/types/modes/types.d.ts +2 -1
  71. package/dist/types/session/agent-session.d.ts +1 -1
  72. package/dist/types/session/auth-broker-config.d.ts +4 -0
  73. package/dist/types/session/session-manager.d.ts +1 -1
  74. package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
  75. package/dist/types/ssh/connection-manager.d.ts +8 -0
  76. package/dist/types/task/discovery.d.ts +1 -2
  77. package/dist/types/task/parallel.d.ts +2 -2
  78. package/dist/types/task/worktree.d.ts +2 -0
  79. package/dist/types/tiny/title-client.d.ts +1 -1
  80. package/dist/types/tools/ask.d.ts +4 -0
  81. package/dist/types/tools/conflict-detect.d.ts +16 -0
  82. package/dist/types/tools/github-cache.d.ts +7 -0
  83. package/dist/types/tools/sqlite-reader.d.ts +3 -0
  84. package/dist/types/tools/todo.d.ts +2 -0
  85. package/dist/types/tui/output-block.d.ts +3 -3
  86. package/dist/types/utils/changelog.d.ts +8 -0
  87. package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
  88. package/dist/types/web/scrapers/types.d.ts +12 -0
  89. package/dist/types/web/search/providers/codex.d.ts +1 -1
  90. package/dist/types/web/search/providers/gemini.d.ts +1 -1
  91. package/examples/extensions/tools.ts +5 -4
  92. package/package.json +14 -11
  93. package/scripts/build-binary.ts +18 -23
  94. package/scripts/bundle-dist.ts +81 -0
  95. package/scripts/{dev-launch → omp} +1 -1
  96. package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
  97. package/src/async/job-manager.ts +57 -3
  98. package/src/autoresearch/dashboard.ts +1 -1
  99. package/src/autoresearch/prompt-setup.md +6 -6
  100. package/src/autoresearch/prompt.md +6 -6
  101. package/src/capability/fs.ts +10 -0
  102. package/src/cli/args.ts +1 -1
  103. package/src/cli/auth-gateway-cli.ts +1 -3
  104. package/src/cli/dry-balance-cli.ts +1 -1
  105. package/src/cli/gallery-cli.ts +1 -1
  106. package/src/cli/gallery-fixtures/fs.ts +1 -1
  107. package/src/cli/gallery-fixtures/types.ts +5 -1
  108. package/src/cli/list-models.ts +7 -12
  109. package/src/cli/usage-cli.ts +603 -0
  110. package/src/cli-commands.ts +1 -0
  111. package/src/cli.ts +69 -5
  112. package/src/commands/complete.ts +1 -1
  113. package/src/commands/launch.ts +1 -1
  114. package/src/commands/read.ts +6 -3
  115. package/src/commands/usage.ts +35 -0
  116. package/src/commit/agentic/agent.ts +1 -1
  117. package/src/commit/model-selection.ts +1 -1
  118. package/src/config/append-only-context-mode.ts +6 -12
  119. package/src/config/model-discovery.ts +554 -0
  120. package/src/config/model-registry.ts +308 -1025
  121. package/src/config/model-resolver.ts +113 -156
  122. package/src/config/model-roles.ts +74 -0
  123. package/src/config/models-config-schema.ts +57 -8
  124. package/src/config/models-config.ts +129 -0
  125. package/src/config/settings-schema.ts +18 -14
  126. package/src/config/settings.ts +37 -1
  127. package/src/dap/client.ts +124 -37
  128. package/src/dap/session.ts +259 -158
  129. package/src/debug/log-viewer.ts +1 -1
  130. package/src/debug/raw-sse.ts +1 -1
  131. package/src/debug/terminal-info.ts +0 -3
  132. package/src/edit/diff.ts +95 -18
  133. package/src/edit/hashline/block-resolver.ts +20 -1
  134. package/src/edit/hashline/diff.ts +36 -1
  135. package/src/edit/hashline/execute.ts +8 -2
  136. package/src/edit/index.ts +16 -1
  137. package/src/edit/modes/patch.ts +52 -0
  138. package/src/edit/modes/replace.ts +56 -22
  139. package/src/edit/notebook.ts +22 -2
  140. package/src/edit/renderer.ts +36 -10
  141. package/src/eval/__tests__/completion-bridge.test.ts +1 -1
  142. package/src/eval/backend.ts +0 -2
  143. package/src/eval/completion-bridge.ts +2 -1
  144. package/src/eval/idle-timeout.ts +2 -9
  145. package/src/eval/js/context-manager.ts +6 -8
  146. package/src/eval/js/executor.ts +6 -2
  147. package/src/eval/js/index.ts +0 -2
  148. package/src/eval/js/shared/helpers.ts +5 -6
  149. package/src/eval/js/shared/local-module-loader.ts +1 -1
  150. package/src/eval/js/shared/prelude.txt +62 -1
  151. package/src/eval/js/shared/rewrite-imports.ts +49 -23
  152. package/src/eval/js/shared/runtime.ts +1 -1
  153. package/src/eval/py/index.ts +0 -2
  154. package/src/eval/py/kernel.ts +19 -0
  155. package/src/eval/py/runner.py +107 -3
  156. package/src/exec/bash-executor.ts +3 -1
  157. package/src/export/html/template.generated.ts +1 -1
  158. package/src/export/html/template.js +3 -1
  159. package/src/extensibility/extensions/types.ts +3 -2
  160. package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
  161. package/src/hindsight/mental-models.ts +59 -12
  162. package/src/hindsight/state.ts +6 -1
  163. package/src/internal-urls/artifact-protocol.ts +11 -2
  164. package/src/internal-urls/docs-index.generated.ts +10 -10
  165. package/src/internal-urls/issue-pr-protocol.ts +12 -5
  166. package/src/internal-urls/router.ts +1 -1
  167. package/src/internal-urls/types.ts +1 -1
  168. package/src/lib/xai-http.ts +1 -1
  169. package/src/lsp/client.ts +118 -38
  170. package/src/lsp/clients/biome-client.ts +101 -39
  171. package/src/lsp/edits.ts +143 -95
  172. package/src/lsp/index.ts +31 -22
  173. package/src/lsp/render.ts +1 -1
  174. package/src/lsp/types.ts +2 -0
  175. package/src/lsp/utils.ts +28 -10
  176. package/src/main.ts +165 -17
  177. package/src/mcp/json-rpc.ts +35 -5
  178. package/src/mcp/transports/stdio.ts +7 -1
  179. package/src/memories/index.ts +2 -1
  180. package/src/mnemopi/backend.ts +25 -3
  181. package/src/mnemopi/state.ts +38 -2
  182. package/src/modes/components/agent-dashboard.ts +10 -7
  183. package/src/modes/components/assistant-message.ts +19 -13
  184. package/src/modes/components/bash-execution.ts +1 -1
  185. package/src/modes/components/copy-selector.ts +1 -1
  186. package/src/modes/components/diff.ts +13 -2
  187. package/src/modes/components/dynamic-border.ts +12 -3
  188. package/src/modes/components/extensions/extension-dashboard.ts +8 -5
  189. package/src/modes/components/extensions/extension-list.ts +1 -1
  190. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  191. package/src/modes/components/footer.ts +1 -1
  192. package/src/modes/components/history-search.ts +1 -1
  193. package/src/modes/components/hook-editor.ts +8 -0
  194. package/src/modes/components/hook-input.ts +8 -0
  195. package/src/modes/components/hook-selector.ts +2 -2
  196. package/src/modes/components/model-selector.ts +66 -54
  197. package/src/modes/components/plan-review-overlay.ts +1 -1
  198. package/src/modes/components/session-observer-overlay.ts +2 -2
  199. package/src/modes/components/session-selector.ts +1 -1
  200. package/src/modes/components/settings-selector.ts +5 -1
  201. package/src/modes/components/status-line/component.ts +1 -1
  202. package/src/modes/components/tiny-title-download-progress.ts +1 -1
  203. package/src/modes/components/transcript-container.ts +373 -141
  204. package/src/modes/components/tree-selector.ts +3 -3
  205. package/src/modes/components/user-message-selector.ts +1 -1
  206. package/src/modes/components/user-message.ts +17 -5
  207. package/src/modes/components/visual-truncate.ts +1 -1
  208. package/src/modes/components/welcome.ts +108 -26
  209. package/src/modes/controllers/command-controller.ts +10 -3
  210. package/src/modes/controllers/event-controller.ts +73 -49
  211. package/src/modes/controllers/input-controller.ts +5 -5
  212. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  213. package/src/modes/controllers/selector-controller.ts +1 -5
  214. package/src/modes/controllers/streaming-reveal.ts +85 -18
  215. package/src/modes/interactive-mode.ts +5 -19
  216. package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
  217. package/src/modes/setup-wizard/scenes/providers.ts +1 -1
  218. package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
  219. package/src/modes/setup-wizard/scenes/theme.ts +1 -1
  220. package/src/modes/setup-wizard/scenes/types.ts +1 -1
  221. package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
  222. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  223. package/src/modes/types.ts +2 -1
  224. package/src/prompts/agents/explore.md +2 -2
  225. package/src/prompts/agents/librarian.md +1 -2
  226. package/src/prompts/agents/oracle.md +1 -1
  227. package/src/prompts/agents/plan.md +5 -5
  228. package/src/prompts/agents/task.md +5 -5
  229. package/src/prompts/ci-green-request.md +5 -7
  230. package/src/prompts/goals/goal-budget-limit.md +2 -2
  231. package/src/prompts/goals/goal-continuation.md +4 -4
  232. package/src/prompts/goals/goal-mode-active.md +1 -1
  233. package/src/prompts/memories/read-path.md +1 -1
  234. package/src/prompts/memories/stage_one_system.md +2 -2
  235. package/src/prompts/review-custom-request.md +1 -1
  236. package/src/prompts/system/agent-creation-architect.md +2 -2
  237. package/src/prompts/system/auto-continue.md +1 -1
  238. package/src/prompts/system/background-tan-dispatch.md +1 -1
  239. package/src/prompts/system/btw-user.md +2 -2
  240. package/src/prompts/system/commit-message-system.md +13 -1
  241. package/src/prompts/system/custom-system-prompt.md +1 -1
  242. package/src/prompts/system/eager-todo.md +2 -2
  243. package/src/prompts/system/irc-incoming.md +1 -1
  244. package/src/prompts/system/manual-continue.md +1 -1
  245. package/src/prompts/system/omfg-user.md +3 -4
  246. package/src/prompts/system/orchestrate-notice.md +9 -9
  247. package/src/prompts/system/plan-mode-active.md +4 -4
  248. package/src/prompts/system/plan-mode-subagent.md +4 -5
  249. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  250. package/src/prompts/system/project-prompt.md +2 -2
  251. package/src/prompts/system/subagent-system-prompt.md +4 -4
  252. package/src/prompts/system/system-prompt.md +15 -26
  253. package/src/prompts/system/title-system.md +2 -2
  254. package/src/prompts/system/ttsr-tool-reminder.md +1 -1
  255. package/src/prompts/system/workflow-notice.md +1 -1
  256. package/src/prompts/tools/ast-edit.md +1 -1
  257. package/src/prompts/tools/ast-grep.md +2 -2
  258. package/src/prompts/tools/bash.md +8 -10
  259. package/src/prompts/tools/browser.md +7 -7
  260. package/src/prompts/tools/debug.md +1 -1
  261. package/src/prompts/tools/eval.md +3 -3
  262. package/src/prompts/tools/find.md +0 -1
  263. package/src/prompts/tools/github.md +8 -7
  264. package/src/prompts/tools/goal.md +1 -1
  265. package/src/prompts/tools/image-gen.md +1 -1
  266. package/src/prompts/tools/inspect-image-system.md +1 -1
  267. package/src/prompts/tools/irc.md +15 -15
  268. package/src/prompts/tools/lsp.md +2 -2
  269. package/src/prompts/tools/patch.md +2 -2
  270. package/src/prompts/tools/read.md +3 -4
  271. package/src/prompts/tools/recall.md +1 -1
  272. package/src/prompts/tools/reflect.md +1 -1
  273. package/src/prompts/tools/render-mermaid.md +2 -2
  274. package/src/prompts/tools/replace.md +4 -10
  275. package/src/prompts/tools/rewind.md +2 -2
  276. package/src/prompts/tools/search-tool-bm25.md +1 -9
  277. package/src/prompts/tools/search.md +0 -1
  278. package/src/prompts/tools/ssh.md +0 -4
  279. package/src/prompts/tools/task.md +2 -3
  280. package/src/prompts/tools/todo.md +6 -2
  281. package/src/sdk.ts +23 -10
  282. package/src/session/agent-session.ts +44 -10
  283. package/src/session/auth-broker-config.ts +30 -1
  284. package/src/session/session-manager.ts +2 -2
  285. package/src/session/streaming-output.ts +23 -2
  286. package/src/slash-commands/builtin-registry.ts +20 -0
  287. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  288. package/src/ssh/connection-manager.ts +27 -0
  289. package/src/task/commands.ts +2 -1
  290. package/src/task/discovery.ts +17 -24
  291. package/src/task/executor.ts +61 -53
  292. package/src/task/index.ts +137 -60
  293. package/src/task/parallel.ts +3 -3
  294. package/src/task/render.ts +2 -2
  295. package/src/task/worktree.ts +64 -56
  296. package/src/thinking.ts +2 -1
  297. package/src/tiny/title-client.ts +32 -14
  298. package/src/tools/archive-reader.ts +30 -2
  299. package/src/tools/ask.ts +104 -21
  300. package/src/tools/ast-edit.ts +25 -5
  301. package/src/tools/auto-generated-guard.ts +20 -3
  302. package/src/tools/bash-interactive.ts +27 -7
  303. package/src/tools/bash.ts +54 -13
  304. package/src/tools/browser/launch.ts +11 -2
  305. package/src/tools/browser/readable.ts +19 -2
  306. package/src/tools/browser/registry.ts +4 -1
  307. package/src/tools/browser/render.ts +2 -2
  308. package/src/tools/browser/tab-supervisor.ts +55 -16
  309. package/src/tools/conflict-detect.ts +50 -4
  310. package/src/tools/debug.ts +1 -1
  311. package/src/tools/eval-render.ts +5 -5
  312. package/src/tools/eval.ts +0 -2
  313. package/src/tools/fetch.ts +33 -10
  314. package/src/tools/gh-cache-invalidation.ts +63 -8
  315. package/src/tools/gh-renderer.ts +1 -1
  316. package/src/tools/gh.ts +172 -29
  317. package/src/tools/github-cache.ts +70 -6
  318. package/src/tools/image-gen.ts +3 -9
  319. package/src/tools/irc.ts +5 -1
  320. package/src/tools/job.ts +1 -1
  321. package/src/tools/read.ts +202 -61
  322. package/src/tools/render-utils.ts +3 -3
  323. package/src/tools/resolve.ts +1 -1
  324. package/src/tools/search.ts +92 -29
  325. package/src/tools/sqlite-reader.ts +17 -5
  326. package/src/tools/ssh.ts +8 -8
  327. package/src/tools/todo.ts +51 -12
  328. package/src/tools/write.ts +118 -18
  329. package/src/tui/output-block.ts +4 -4
  330. package/src/utils/changelog.ts +27 -1
  331. package/src/utils/file-mentions.ts +2 -1
  332. package/src/web/scrapers/arxiv.ts +1 -1
  333. package/src/web/scrapers/go-pkg.ts +1 -1
  334. package/src/web/scrapers/iacr.ts +1 -1
  335. package/src/web/scrapers/readthedocs.ts +1 -1
  336. package/src/web/scrapers/twitter.ts +2 -1
  337. package/src/web/scrapers/types.ts +87 -8
  338. package/src/web/scrapers/wikipedia.ts +1 -1
  339. package/src/web/scrapers/youtube.ts +6 -1
  340. package/src/web/search/index.ts +1 -1
  341. package/src/web/search/providers/anthropic.ts +8 -2
  342. package/src/web/search/providers/codex.ts +2 -1
  343. package/src/web/search/providers/gemini.ts +2 -3
  344. package/src/web/search/render.ts +8 -6
  345. package/dist/types/config/model-equivalence.d.ts +0 -24
  346. package/dist/types/config/model-id-affixes.d.ts +0 -12
  347. package/dist/types/config/model-provider-priority.d.ts +0 -1
  348. package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
  349. package/src/config/model-equivalence.ts +0 -875
  350. package/src/config/model-id-affixes.ts +0 -81
  351. package/src/config/model-provider-priority.ts +0 -56
  352. package/src/exec/idle-timeout-watchdog.ts +0 -126
package/src/lsp/edits.ts CHANGED
@@ -24,27 +24,7 @@ import { uriToFile } from "./utils";
24
24
  */
25
25
  export function applyTextEditsToString(content: string, edits: TextEdit[]): string {
26
26
  const lines = content.split("\n");
27
-
28
- // Sort edits in reverse order (bottom-to-top, right-to-left)
29
- const sortedEdits = [...edits].sort((a, b) => {
30
- if (a.range.start.line !== b.range.start.line) {
31
- return b.range.start.line - a.range.start.line;
32
- }
33
- return b.range.start.character - a.range.start.character;
34
- });
35
-
36
- // Detect overlapping ranges: in reverse-sorted order, each edit's start
37
- // must be >= the next edit's end. If not, the edits would clobber each other
38
- // once applied bottom-up (typically a multi-server rename with stale positions).
39
- for (let i = 0; i < sortedEdits.length - 1; i++) {
40
- const later = sortedEdits[i].range;
41
- const earlier = sortedEdits[i + 1].range;
42
- if (comparePosition(earlier.end, later.start) > 0) {
43
- throw new ToolError(
44
- `overlapping LSP edits: ${formatRange(earlier)} conflicts with ${formatRange(later)}; multi-server rename produced inconsistent edits`,
45
- );
46
- }
47
- }
27
+ const sortedEdits = sortAndValidateTextEdits(edits);
48
28
 
49
29
  for (const edit of sortedEdits) {
50
30
  const { start, end } = edit.range;
@@ -78,6 +58,42 @@ export function rangesOverlap(a: Range, b: Range): boolean {
78
58
  return comparePosition(a.start, b.end) < 0 && comparePosition(b.start, a.end) < 0;
79
59
  }
80
60
 
61
+ /**
62
+ * Sort edits bottom-to-top for in-place application and reject overlaps.
63
+ * Equal start positions tiebreak by original array index descending so that,
64
+ * applied bottom-up, inserts at the same position land in array order
65
+ * (LSP spec: the order of edits in the array defines the order in the result).
66
+ */
67
+ export function sortAndValidateTextEdits(edits: TextEdit[]): TextEdit[] {
68
+ const sorted = edits
69
+ .map((edit, index) => ({ edit, index }))
70
+ .sort((a, b) => {
71
+ if (a.edit.range.start.line !== b.edit.range.start.line) {
72
+ return b.edit.range.start.line - a.edit.range.start.line;
73
+ }
74
+ if (a.edit.range.start.character !== b.edit.range.start.character) {
75
+ return b.edit.range.start.character - a.edit.range.start.character;
76
+ }
77
+ return b.index - a.index;
78
+ })
79
+ .map(entry => entry.edit);
80
+
81
+ // Detect overlapping ranges: in reverse-sorted order, each edit's start
82
+ // must be >= the next edit's end. If not, the edits would clobber each other
83
+ // once applied bottom-up (typically a multi-server rename with stale positions).
84
+ for (let i = 0; i < sorted.length - 1; i++) {
85
+ const later = sorted[i].range;
86
+ const earlier = sorted[i + 1].range;
87
+ if (comparePosition(earlier.end, later.start) > 0) {
88
+ throw new ToolError(
89
+ `overlapping LSP edits: ${formatRange(earlier)} conflicts with ${formatRange(later)}; multi-server rename produced inconsistent edits`,
90
+ );
91
+ }
92
+ }
93
+
94
+ return sorted;
95
+ }
96
+
81
97
  /**
82
98
  * Flatten a WorkspaceEdit's text edits into a Map<uri, TextEdit[]>.
83
99
  * Resource operations (create/rename/delete) are ignored — callers handle them separately.
@@ -120,92 +136,124 @@ export async function applyTextEdits(filePath: string, edits: TextEdit[]): Promi
120
136
  // Workspace Edit Application
121
137
  // =============================================================================
122
138
 
139
+ type WorkspaceEditOp =
140
+ | { kind: "text"; uri: string; edits: TextEdit[] }
141
+ | { kind: "create"; uri: string }
142
+ | { kind: "rename"; oldUri: string; newUri: string }
143
+ | { kind: "delete"; uri: string };
144
+
145
+ /**
146
+ * Flatten documentChanges into an ordered op list. Text edits are accumulated
147
+ * per-URI and flushed before any resource op that touches the same URI (or,
148
+ * for folder rename/delete, any descendant URI) so that renames, creates, and
149
+ * deletes always see the correct prior file state.
150
+ */
151
+ function planDocumentChanges(documentChanges: NonNullable<WorkspaceEdit["documentChanges"]>): WorkspaceEditOp[] {
152
+ const ops: WorkspaceEditOp[] = [];
153
+ const pending = new Map<string, TextEdit[]>();
154
+
155
+ const flushUri = (uri: string) => {
156
+ const edits = pending.get(uri);
157
+ if (!edits) return;
158
+ pending.delete(uri);
159
+ ops.push({ kind: "text", uri, edits });
160
+ };
161
+
162
+ // Flush the exact URI plus every pending descendant (for folder-level
163
+ // resource ops where the queued edits target child files of the target).
164
+ const flushSubtree = (uri: string) => {
165
+ const prefix = uri.endsWith("/") ? uri : `${uri}/`;
166
+ const matches: string[] = [];
167
+ for (const candidate of pending.keys()) {
168
+ if (candidate === uri || candidate.startsWith(prefix)) matches.push(candidate);
169
+ }
170
+ for (const target of matches) {
171
+ flushUri(target);
172
+ }
173
+ };
174
+
175
+ for (const change of documentChanges) {
176
+ if ("textDocument" in change && change.textDocument && "edits" in change && change.edits) {
177
+ const tdc = change as TextDocumentEdit;
178
+ const uri = tdc.textDocument.uri;
179
+ const textEdits = tdc.edits.filter((e): e is TextEdit => "range" in e && "newText" in e);
180
+ if (textEdits.length > 0) {
181
+ const prev = pending.get(uri);
182
+ if (prev) prev.push(...textEdits);
183
+ else pending.set(uri, [...textEdits]);
184
+ }
185
+ } else if ("kind" in change && change.kind) {
186
+ if (change.kind === "create") {
187
+ const createOp = change as CreateFile;
188
+ flushUri(createOp.uri);
189
+ ops.push({ kind: "create", uri: createOp.uri });
190
+ } else if (change.kind === "rename") {
191
+ const renameOp = change as RenameFile;
192
+ // Per LSP §3.16.2 documentChanges are applied in declared order.
193
+ // Flush both the source subtree (so prior edits land before the move)
194
+ // AND the destination subtree (so prior edits land on whatever exists
195
+ // at newUri before the rename overwrites/replaces it — relevant under
196
+ // `options.overwrite` and `options.ignoreIfExists`).
197
+ flushSubtree(renameOp.oldUri);
198
+ flushSubtree(renameOp.newUri);
199
+ ops.push({ kind: "rename", oldUri: renameOp.oldUri, newUri: renameOp.newUri });
200
+ } else if (change.kind === "delete") {
201
+ const deleteOp = change as DeleteFile;
202
+ flushSubtree(deleteOp.uri);
203
+ ops.push({ kind: "delete", uri: deleteOp.uri });
204
+ }
205
+ }
206
+ }
207
+
208
+ // Flush text edits not followed by a resource op.
209
+ for (const uri of [...pending.keys()]) {
210
+ flushUri(uri);
211
+ }
212
+
213
+ return ops;
214
+ }
215
+
123
216
  /**
124
217
  * Apply a workspace edit (collection of file changes).
218
+ * All text-edit batches are overlap-validated before anything is written so a
219
+ * conflict throws without leaving the workspace half-applied.
125
220
  * Returns array of applied change descriptions.
126
221
  */
127
222
  export async function applyWorkspaceEdit(edit: WorkspaceEdit, cwd: string): Promise<string[]> {
128
223
  const applied: string[] = [];
129
224
 
130
225
  if (edit.documentChanges) {
131
- // Walk documentChanges in original order. Accumulate text edits per-URI and
132
- // flush them before any resource op that touches the same URI (or, for folder
133
- // rename/delete, any descendant URI) so that renames, creates, and deletes
134
- // always see the correct prior file state.
135
- const pending = new Map<string, TextEdit[]>();
136
-
137
- const flushUri = async (uri: string) => {
138
- const edits = pending.get(uri);
139
- if (!edits) return;
140
- pending.delete(uri);
141
- const filePath = uriToFile(uri);
142
- await applyTextEdits(filePath, edits);
143
- applied.push(`Applied ${edits.length} edit(s) to ${formatPathRelativeToCwd(filePath, cwd)}`);
144
- };
145
-
146
- // Flush the exact URI plus every pending descendant (for folder-level
147
- // resource ops where the queued edits target child files of the target).
148
- const flushSubtree = async (uri: string) => {
149
- const prefix = uri.endsWith("/") ? uri : `${uri}/`;
150
- const matches: string[] = [];
151
- for (const candidate of pending.keys()) {
152
- if (candidate === uri || candidate.startsWith(prefix)) matches.push(candidate);
153
- }
154
- for (const target of matches) {
155
- await flushUri(target);
156
- }
157
- };
158
-
159
- for (const change of edit.documentChanges) {
160
- if ("textDocument" in change && change.textDocument && "edits" in change && change.edits) {
161
- const tdc = change as TextDocumentEdit;
162
- const uri = tdc.textDocument.uri;
163
- const textEdits = tdc.edits.filter((e): e is TextEdit => "range" in e && "newText" in e);
164
- if (textEdits.length > 0) {
165
- const prev = pending.get(uri);
166
- if (prev) prev.push(...textEdits);
167
- else pending.set(uri, [...textEdits]);
168
- }
169
- } else if ("kind" in change && change.kind) {
170
- if (change.kind === "create") {
171
- const createOp = change as CreateFile;
172
- await flushUri(createOp.uri);
173
- const filePath = uriToFile(createOp.uri);
174
- await Bun.write(filePath, "");
175
- applied.push(`Created ${formatPathRelativeToCwd(filePath, cwd)}`);
176
- } else if (change.kind === "rename") {
177
- const renameOp = change as RenameFile;
178
- // Per LSP §3.16.2 documentChanges are applied in declared order.
179
- // Flush both the source subtree (so prior edits land before the move)
180
- // AND the destination subtree (so prior edits land on whatever exists
181
- // at newUri before the rename overwrites/replaces it — relevant under
182
- // `options.overwrite` and `options.ignoreIfExists`).
183
- await flushSubtree(renameOp.oldUri);
184
- await flushSubtree(renameOp.newUri);
185
- const oldPath = uriToFile(renameOp.oldUri);
186
- const newPath = uriToFile(renameOp.newUri);
187
- await fs.mkdir(path.dirname(newPath), { recursive: true });
188
- await fs.rename(oldPath, newPath);
189
- applied.push(
190
- `Renamed ${formatPathRelativeToCwd(oldPath, cwd)} → ${formatPathRelativeToCwd(newPath, cwd)}`,
191
- );
192
- } else if (change.kind === "delete") {
193
- const deleteOp = change as DeleteFile;
194
- await flushSubtree(deleteOp.uri);
195
- const filePath = uriToFile(deleteOp.uri);
196
- await fs.rm(filePath, { recursive: true });
197
- applied.push(`Deleted ${formatPathRelativeToCwd(filePath, cwd)}`);
198
- }
199
- }
226
+ const ops = planDocumentChanges(edit.documentChanges);
227
+ for (const op of ops) {
228
+ if (op.kind === "text") sortAndValidateTextEdits(op.edits);
200
229
  }
201
-
202
- // Flush text edits not followed by a resource op.
203
- for (const [uri] of pending) {
204
- await flushUri(uri);
230
+ for (const op of ops) {
231
+ if (op.kind === "text") {
232
+ const filePath = uriToFile(op.uri);
233
+ await applyTextEdits(filePath, op.edits);
234
+ applied.push(`Applied ${op.edits.length} edit(s) to ${formatPathRelativeToCwd(filePath, cwd)}`);
235
+ } else if (op.kind === "create") {
236
+ const filePath = uriToFile(op.uri);
237
+ await Bun.write(filePath, "");
238
+ applied.push(`Created ${formatPathRelativeToCwd(filePath, cwd)}`);
239
+ } else if (op.kind === "rename") {
240
+ const oldPath = uriToFile(op.oldUri);
241
+ const newPath = uriToFile(op.newUri);
242
+ await fs.mkdir(path.dirname(newPath), { recursive: true });
243
+ await fs.rename(oldPath, newPath);
244
+ applied.push(`Renamed ${formatPathRelativeToCwd(oldPath, cwd)} → ${formatPathRelativeToCwd(newPath, cwd)}`);
245
+ } else {
246
+ const filePath = uriToFile(op.uri);
247
+ await fs.rm(filePath, { recursive: true });
248
+ applied.push(`Deleted ${formatPathRelativeToCwd(filePath, cwd)}`);
249
+ }
205
250
  }
206
251
  } else if (edit.changes) {
207
- // Legacy changes-map path: apply all text edits in one pass.
252
+ // Legacy changes-map path: validate every file's edits before writing any.
208
253
  const changes = edit.changes;
254
+ for (const uri in changes) {
255
+ sortAndValidateTextEdits(changes[uri]);
256
+ }
209
257
  for (const uri in changes) {
210
258
  const textEdits = changes[uri];
211
259
  if (textEdits.length === 0) continue;
package/src/lsp/index.ts CHANGED
@@ -105,7 +105,7 @@ export const LSP_READONLY_ACTIONS: ReadonlySet<string> = new Set([
105
105
 
106
106
  export interface LspStartupServerInfo {
107
107
  name: string;
108
- status: "connecting" | "ready" | "error";
108
+ status: "connecting" | "ready" | "error" | "available";
109
109
  fileTypes: string[];
110
110
  error?: string;
111
111
  }
@@ -121,11 +121,14 @@ export interface LspWarmupOptions {
121
121
  onConnecting?: (serverNames: string[]) => void;
122
122
  }
123
123
 
124
- export function discoverStartupLspServers(cwd: string): LspStartupServerInfo[] {
124
+ export function discoverStartupLspServers(
125
+ cwd: string,
126
+ status: LspStartupServerInfo["status"] = "connecting",
127
+ ): LspStartupServerInfo[] {
125
128
  const config = loadConfig(cwd);
126
129
  return getLspServers(config).map(([name, serverConfig]) => ({
127
130
  name,
128
- status: "connecting",
131
+ status,
129
132
  fileTypes: serverConfig.fileTypes,
130
133
  }));
131
134
  }
@@ -454,21 +457,23 @@ function isMethodNotFoundError(err: unknown): boolean {
454
457
  }
455
458
 
456
459
  async function reloadServer(client: LspClient, serverName: string, signal?: AbortSignal): Promise<string> {
457
- let output = `Restarted ${serverName}`;
458
- const reloadMethods = ["rust-analyzer/reloadWorkspace", "workspace/didChangeConfiguration"];
459
- for (const method of reloadMethods) {
460
- try {
461
- await sendRequest(client, method, method.includes("Configuration") ? { settings: {} } : null, signal);
462
- output = `Reloaded ${serverName}`;
463
- break;
464
- } catch {
465
- // Method not supported, try next
466
- }
460
+ // rust-analyzer exposes a real reload request.
461
+ try {
462
+ await sendRequest(client, "rust-analyzer/reloadWorkspace", null, signal);
463
+ return `Reloaded ${serverName}`;
464
+ } catch {
465
+ // Method not supported — fall through.
467
466
  }
468
- if (output.startsWith("Restarted")) {
467
+ // workspace/didChangeConfiguration is a notification per spec; sending it
468
+ // as a request hangs until the tool deadline on servers that route it to
469
+ // the notification handler and never respond.
470
+ try {
471
+ await sendNotification(client, "workspace/didChangeConfiguration", { settings: {} });
472
+ return `Reloaded ${serverName}`;
473
+ } catch {
469
474
  client.proc.kill();
475
+ return `Restarted ${serverName}`;
470
476
  }
471
- return output;
472
477
  }
473
478
 
474
479
  interface WaitForDiagnosticsOptions {
@@ -636,12 +641,13 @@ interface GetDiagnosticsForFileOptions {
636
641
  async function captureDiagnosticVersions(
637
642
  cwd: string,
638
643
  servers: Array<[string, ServerConfig]>,
644
+ initTimeoutMs?: number,
639
645
  ): Promise<ServerVersionMap> {
640
646
  const versions = new Map<string, number>();
641
647
  await Promise.allSettled(
642
648
  servers.map(async ([serverName, serverConfig]) => {
643
649
  if (serverConfig.createClient) return;
644
- const client = await getOrCreateClient(serverConfig, cwd);
650
+ const client = await getOrCreateClient(serverConfig, cwd, initTimeoutMs);
645
651
  versions.set(serverName, client.diagnosticsVersion);
646
652
  }),
647
653
  );
@@ -1118,7 +1124,9 @@ async function runLspWritethrough(
1118
1124
  const useCustomFormatter = enableFormat && customLinterServers.length > 0;
1119
1125
 
1120
1126
  // Capture diagnostic versions BEFORE syncing to detect stale diagnostics
1121
- const minVersions = enableDiagnostics ? await captureDiagnosticVersions(cwd, servers) : undefined;
1127
+ // Bound client creation by the writethrough budget: a hung/broken server
1128
+ // must not add its full init wait (30s default) to every edit.
1129
+ const minVersions = enableDiagnostics ? await captureDiagnosticVersions(cwd, servers, 5_000) : undefined;
1122
1130
  let expectedDocumentVersions: ServerVersionMap | undefined;
1123
1131
 
1124
1132
  let formatter: FileFormatResult | undefined;
@@ -2311,11 +2319,12 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
2311
2319
  break;
2312
2320
  }
2313
2321
  const parsedIndex = /^\d+$/.test(normalizedQuery) ? Number.parseInt(normalizedQuery, 10) : null;
2314
- const selectedAction = result.find(
2315
- (actionItem, index) =>
2316
- (parsedIndex !== null && index === parsedIndex) ||
2317
- actionItem.title.toLowerCase().includes(normalizedQuery.toLowerCase()),
2318
- );
2322
+ const selectedAction =
2323
+ parsedIndex !== null
2324
+ ? result[parsedIndex]
2325
+ : result.find(actionItem =>
2326
+ actionItem.title.toLowerCase().includes(normalizedQuery.toLowerCase()),
2327
+ );
2319
2328
 
2320
2329
  if (!selectedAction) {
2321
2330
  const actionLines = result.map((actionItem, index) => ` ${formatCodeAction(actionItem, index)}`);
package/src/lsp/render.ts CHANGED
@@ -139,7 +139,7 @@ export function renderResult(
139
139
  const outputBlock = new CachedOutputBlock();
140
140
 
141
141
  return markFramedBlockComponent({
142
- render(width: number): string[] {
142
+ render(width: number): readonly string[] {
143
143
  // Read mutable state at render time
144
144
  const { expanded, isPartial, spinnerFrame } = options;
145
145
 
package/src/lsp/types.ts CHANGED
@@ -416,6 +416,8 @@ export interface LspClient {
416
416
  pendingRequests: Map<number, PendingRequest>;
417
417
  messageBuffer: Uint8Array;
418
418
  isReading: boolean;
419
+ /** Lifecycle state: "connecting" until initialize completes, then "ready"; "error" on init failure or reader death. */
420
+ status: "connecting" | "ready" | "error";
419
421
  serverCapabilities?: LspServerCapabilities;
420
422
  lastActivity: number;
421
423
  /** Serializes outbound JSON-RPC writes to the server process. */
package/src/lsp/utils.ts CHANGED
@@ -27,22 +27,17 @@ export { detectLanguageId } from "../utils/lang-from-path";
27
27
 
28
28
  /**
29
29
  * Convert a file path to a file:// URI.
30
+ * Uses the URL machinery so special characters (`%`, `#`, `?`, spaces) are
31
+ * percent-encoded; plain concatenation produced URIs that broke round-trips.
30
32
  * Handles Windows drive letters correctly.
31
33
  */
32
34
  export function fileToUri(filePath: string): string {
33
- const resolved = path.resolve(filePath);
34
-
35
- if (process.platform === "win32") {
36
- // Windows: file:///C:/path/to/file
37
- return `file:///${resolved.replace(/\\/g, "/")}`;
38
- }
39
-
40
- // Unix: file:///path/to/file
41
- return `file://${resolved}`;
35
+ return Bun.pathToFileURL(path.resolve(filePath)).href;
42
36
  }
43
37
 
44
38
  /**
45
39
  * Convert a file:// URI to a file path.
40
+ * Tolerates both percent-encoded URIs and lax servers that send raw paths.
46
41
  * Handles Windows drive letters correctly.
47
42
  */
48
43
  export function uriToFile(uri: string): string {
@@ -50,7 +45,30 @@ export function uriToFile(uri: string): string {
50
45
  return uri;
51
46
  }
52
47
 
53
- let filePath = decodeURIComponent(uri.slice(7));
48
+ // A raw `#`/`?` parses *successfully* as fragment/query and silently
49
+ // truncates the path — it never reaches the catch below. LSP servers do
50
+ // not use fragments or queries on file URIs (encoded forms are %23/%3F),
51
+ // so raw occurrences mean a lax server sent an unencoded path.
52
+ if (uri.includes("#") || uri.includes("?")) {
53
+ return laxUriToFile(uri);
54
+ }
55
+
56
+ try {
57
+ return Bun.fileURLToPath(uri);
58
+ } catch {
59
+ // Not a well-formed file URL (unencoded characters, stray `%`, host
60
+ // component). Fall back to a lenient manual conversion.
61
+ return laxUriToFile(uri);
62
+ }
63
+ }
64
+
65
+ function laxUriToFile(uri: string): string {
66
+ let filePath = uri.slice(7);
67
+ try {
68
+ filePath = decodeURIComponent(filePath);
69
+ } catch {
70
+ // Invalid percent-encoding — treat as a literal path.
71
+ }
54
72
 
55
73
  // Windows: file:///C:/path → C:/path (strip leading slash before drive letter)
56
74
  if (process.platform === "win32" && filePath.startsWith("/") && /^[A-Za-z]:/.test(filePath.slice(1))) {