@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
@@ -112,6 +112,10 @@ const INTERNAL_TOTAL_CAP = 2000;
112
112
  * silently returns no matches for files larger than this; surface a warning
113
113
  * when the caller explicitly targeted such a file so they know to chunk it. */
114
114
  const NATIVE_GREP_MAX_FILE_BYTES = 4 * 1024 * 1024;
115
+ /** Wall-clock budget for a single native grep invocation. Without it, an
116
+ * aborted or runaway search (huge tree, network mount) keeps burning CPU on
117
+ * the native thread pool after the JS promise is abandoned. */
118
+ const SEARCH_GREP_TIMEOUT_MS = 30_000;
115
119
 
116
120
  /**
117
121
  * Parsed `paths` entry — a path (possibly archive-shaped) plus an optional
@@ -351,6 +355,8 @@ function makeVirtualMatch(
351
355
  lineIndex: number,
352
356
  contextBefore: number,
353
357
  contextAfter: number,
358
+ lastEmittedLine: number,
359
+ nextMatchLine: number,
354
360
  ): GrepMatch {
355
361
  const lineNumber = lineIndex + 1;
356
362
  const { text, wasTruncated } = truncateLine(lines[lineIndex] ?? "", DEFAULT_MAX_COLUMN);
@@ -363,7 +369,9 @@ function makeVirtualMatch(
363
369
 
364
370
  if (contextBefore > 0) {
365
371
  const before: NonNullable<GrepMatch["contextBefore"]> = [];
366
- const start = Math.max(0, lineIndex - contextBefore);
372
+ // Start after the previous match's last emitted line so adjacent matches
373
+ // never repeat or rewind context lines (mirrors native grep's sink).
374
+ const start = Math.max(0, lineIndex - contextBefore, lastEmittedLine);
367
375
  for (let idx = start; idx < lineIndex; idx++) {
368
376
  const contextLineNumber = idx + 1;
369
377
  if (lineAllowed(contextLineNumber, resource.ranges)) {
@@ -375,7 +383,8 @@ function makeVirtualMatch(
375
383
 
376
384
  if (contextAfter > 0) {
377
385
  const after: NonNullable<GrepMatch["contextAfter"]> = [];
378
- const end = Math.min(lines.length - 1, lineIndex + contextAfter);
386
+ // Stop before the next match line; it is emitted as a match itself.
387
+ const end = Math.min(lines.length - 1, lineIndex + contextAfter, nextMatchLine - 2);
379
388
  for (let idx = lineIndex + 1; idx <= end; idx++) {
380
389
  const contextLineNumber = idx + 1;
381
390
  if (lineAllowed(contextLineNumber, resource.ranges)) {
@@ -388,6 +397,38 @@ function makeVirtualMatch(
388
397
  return match;
389
398
  }
390
399
 
400
+ /** Build matches for ascending matched line indexes with forward-only,
401
+ * deduplicated context windows (line numbers never repeat or go backwards
402
+ * within one resource). */
403
+ function buildVirtualMatches(
404
+ resource: VirtualSearchResource,
405
+ lines: readonly string[],
406
+ matchedIndexes: readonly number[],
407
+ contextBefore: number,
408
+ contextAfter: number,
409
+ maxCount: number,
410
+ ): GrepMatch[] {
411
+ const matches: GrepMatch[] = [];
412
+ let lastEmittedLine = 0;
413
+ for (let i = 0; i < matchedIndexes.length && matches.length < maxCount; i++) {
414
+ const lineIndex = matchedIndexes[i];
415
+ const nextMatchLine = i + 1 < matchedIndexes.length ? matchedIndexes[i + 1] + 1 : Number.POSITIVE_INFINITY;
416
+ const match = makeVirtualMatch(
417
+ resource,
418
+ lines,
419
+ lineIndex,
420
+ contextBefore,
421
+ contextAfter,
422
+ lastEmittedLine,
423
+ nextMatchLine,
424
+ );
425
+ const after = match.contextAfter;
426
+ lastEmittedLine = after && after.length > 0 ? after[after.length - 1].lineNumber : match.lineNumber;
427
+ matches.push(match);
428
+ }
429
+ return matches;
430
+ }
431
+
391
432
  function compileVirtualRegex(pattern: string, ignoreCase: boolean, multiline: boolean): RegExp {
392
433
  const flags = `${ignoreCase ? "i" : ""}${multiline ? "gm" : ""}`;
393
434
  try {
@@ -406,24 +447,18 @@ function searchVirtualResourceLines(
406
447
  maxCount: number,
407
448
  ): { matches: GrepMatch[]; totalMatches: number; limitReached: boolean } {
408
449
  const lines = splitSearchLines(resource.content);
409
- const matches: GrepMatch[] = [];
410
- let totalMatches = 0;
411
- let limitReached = false;
450
+ const matchedIndexes: number[] = [];
412
451
 
413
452
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
414
453
  const lineNumber = lineIndex + 1;
415
454
  if (!lineAllowed(lineNumber, resource.ranges)) continue;
416
455
  regex.lastIndex = 0;
417
456
  if (!regex.test(lines[lineIndex] ?? "")) continue;
418
- totalMatches++;
419
- if (matches.length >= maxCount) {
420
- limitReached = true;
421
- continue;
422
- }
423
- matches.push(makeVirtualMatch(resource, lines, lineIndex, contextBefore, contextAfter));
457
+ matchedIndexes.push(lineIndex);
424
458
  }
425
459
 
426
- return { matches, totalMatches, limitReached };
460
+ const matches = buildVirtualMatches(resource, lines, matchedIndexes, contextBefore, contextAfter, maxCount);
461
+ return { matches, totalMatches: matchedIndexes.length, limitReached: matchedIndexes.length > matches.length };
427
462
  }
428
463
 
429
464
  function searchVirtualResourceMultiline(
@@ -434,10 +469,8 @@ function searchVirtualResourceMultiline(
434
469
  maxCount: number,
435
470
  ): { matches: GrepMatch[]; totalMatches: number; limitReached: boolean } {
436
471
  const indexed = indexSearchLines(resource.content);
437
- const matches: GrepMatch[] = [];
438
472
  const matchedLines = new Set<number>();
439
- let totalMatches = 0;
440
- let limitReached = false;
473
+ const matchedIndexes: number[] = [];
441
474
 
442
475
  while (true) {
443
476
  const match = regex.exec(resource.content);
@@ -447,12 +480,7 @@ function searchVirtualResourceMultiline(
447
480
  const lineNumber = lineIndex + 1;
448
481
  if (!matchedLines.has(lineNumber) && lineAllowed(lineNumber, resource.ranges)) {
449
482
  matchedLines.add(lineNumber);
450
- totalMatches++;
451
- if (matches.length >= maxCount) {
452
- limitReached = true;
453
- } else {
454
- matches.push(makeVirtualMatch(resource, indexed.lines, lineIndex, contextBefore, contextAfter));
455
- }
483
+ matchedIndexes.push(lineIndex);
456
484
  }
457
485
  }
458
486
  if (match[0].length === 0) {
@@ -460,7 +488,8 @@ function searchVirtualResourceMultiline(
460
488
  }
461
489
  }
462
490
 
463
- return { matches, totalMatches, limitReached };
491
+ const matches = buildVirtualMatches(resource, indexed.lines, matchedIndexes, contextBefore, contextAfter, maxCount);
492
+ return { matches, totalMatches: matchedIndexes.length, limitReached: matchedIndexes.length > matches.length };
464
493
  }
465
494
 
466
495
  function searchVirtualResources(
@@ -666,10 +695,12 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
666
695
  const { pattern, paths: rawPaths, i, gitignore, skip } = params;
667
696
 
668
697
  return untilAborted(signal, async () => {
669
- const normalizedPattern = pattern.trim();
670
- if (!normalizedPattern) {
698
+ // Preserve the pattern verbatim — leading/trailing whitespace is
699
+ // meaningful in regexes (indentation anchors, trailing-space matches).
700
+ if (!pattern.trim()) {
671
701
  throw new ToolError("Pattern must not be empty");
672
702
  }
703
+ const normalizedPattern = pattern;
673
704
 
674
705
  const normalizedSkip =
675
706
  skip === undefined || skip === null ? 0 : Number.isFinite(skip) ? Math.floor(skip) : Number.NaN;
@@ -729,7 +760,11 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
729
760
  }
730
761
  }
731
762
 
732
- if (archiveUnreadable.length > 0 && searchablePaths.length === archiveUnreadable.length) {
763
+ if (
764
+ archiveUnreadable.length > 0 &&
765
+ searchablePaths.length === archiveUnreadable.length &&
766
+ virtualResources.length === 0
767
+ ) {
733
768
  // All inputs were archive selectors we couldn't materialize; surface the
734
769
  // reason instead of a downstream "path not found" from the scope resolver.
735
770
  throw new ToolError(
@@ -823,6 +858,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
823
858
  filesSearched: 0,
824
859
  limitReached: false,
825
860
  };
861
+ let skippedOversizedCount = 0;
826
862
  try {
827
863
  if (searchablePaths.length > 0) {
828
864
  if (exactFilePaths || multiTargets) {
@@ -852,9 +888,13 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
852
888
  contextAfter: normalizedContextAfter,
853
889
  maxColumns: DEFAULT_MAX_COLUMN,
854
890
  mode: effectiveOutputMode,
891
+ maxCountPerFile: perFileMatchCap + 1,
892
+ signal,
893
+ timeoutMs: SEARCH_GREP_TIMEOUT_MS,
855
894
  },
856
895
  undefined,
857
896
  );
897
+ skippedOversizedCount += targetResult.skippedOversized ?? 0;
858
898
  limitReached = limitReached || Boolean(targetResult.limitReached);
859
899
  totalMatches += targetResult.totalMatches;
860
900
  filesSearched += targetResult.filesSearched;
@@ -887,15 +927,24 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
887
927
  contextAfter: normalizedContextAfter,
888
928
  maxColumns: DEFAULT_MAX_COLUMN,
889
929
  mode: effectiveOutputMode,
930
+ maxCountPerFile: perFileMatchCap + 1,
931
+ signal,
932
+ timeoutMs: SEARCH_GREP_TIMEOUT_MS,
890
933
  },
891
934
  undefined,
892
935
  );
936
+ skippedOversizedCount = result.skippedOversized ?? 0;
893
937
  }
894
938
  }
895
939
  } catch (err) {
896
940
  if (err instanceof Error && /^regex(?: parse)? error/i.test(err.message)) {
897
941
  throw new ToolError(err.message.replace(/^regex(?: parse)? error:?\s*/i, "Invalid regex: "));
898
942
  }
943
+ if (err instanceof Error && err.message.includes("Aborted: Timeout")) {
944
+ throw new ToolError(
945
+ `Search timed out after ${SEARCH_GREP_TIMEOUT_MS / 1000}s; narrow paths or pattern, or scope with \`find\` first`,
946
+ );
947
+ }
899
948
  throw err;
900
949
  }
901
950
  const virtualResult = searchVirtualResources(
@@ -971,6 +1020,9 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
971
1020
  }
972
1021
  }
973
1022
  const totalFiles = fileOrder.length;
1023
+ // When native grep stopped at its internal cap, files past the cap were
1024
+ // never surfaced — the file total is only a lower bound.
1025
+ const totalFilesLabel = result.limitReached ? `${totalFiles}+` : `${totalFiles}`;
974
1026
  // Single-file scopes can't paginate — there is one file by definition.
975
1027
  const canPaginate = isMultiScope;
976
1028
  const skipFiles = canPaginate ? Math.min(normalizedSkip, totalFiles) : 0;
@@ -993,7 +1045,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
993
1045
  }
994
1046
  const nextSkip = skipFiles + windowFiles.length;
995
1047
  const limitMessage = fileLimitReached
996
- ? `Showing files ${skipFiles + 1}-${nextSkip} of ${totalFiles}. Use skip=${nextSkip} for the next page, or narrow paths/pattern.`
1048
+ ? `Showing files ${skipFiles + 1}-${nextSkip} of ${totalFilesLabel}. Use skip=${nextSkip} for the next page, or narrow paths/pattern.`
997
1049
  : "";
998
1050
  const { record: recordFile, list: fileList } = createFileRecorder();
999
1051
  const fileMatchCounts = new Map<string, number>();
@@ -1025,6 +1077,12 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
1025
1077
  const limitMb = Math.floor(NATIVE_GREP_MAX_FILE_BYTES / (1024 * 1024));
1026
1078
  return `Skipped oversized files (>${limitMb}MB grep limit; split the file or narrow with \`read\`): ${oversized.join(", ")}`;
1027
1079
  })();
1080
+ // Directory/multi-target scopes: native grep counts oversized skips but
1081
+ // cannot name them; explicit-file scopes are covered (with names) above.
1082
+ const oversizedScanNote =
1083
+ !oversizedNote && skippedOversizedCount > 0
1084
+ ? `Skipped ${skippedOversizedCount} oversized file(s) (>${Math.floor(NATIVE_GREP_MAX_FILE_BYTES / (1024 * 1024))}MB grep limit); target them directly with \`read\``
1085
+ : undefined;
1028
1086
  const archiveNote =
1029
1087
  archiveUnreadable.length > 0
1030
1088
  ? `Skipped archive entries (search supports text members only): ${archiveUnreadable.join(", ")}`
@@ -1036,8 +1094,9 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
1036
1094
  const missingPathsNote =
1037
1095
  missingPathsForNote.length > 0 ? `Skipped missing paths: ${missingPathsForNote.join(", ")}` : undefined;
1038
1096
  const warningNote =
1039
- [missingPathsNote, archiveNote, oversizedNote].filter((s): s is string => Boolean(s)).join("\n") ||
1040
- undefined;
1097
+ [missingPathsNote, archiveNote, oversizedNote, oversizedScanNote]
1098
+ .filter((s): s is string => Boolean(s))
1099
+ .join("\n") || undefined;
1041
1100
  if (selectedMatches.length === 0) {
1042
1101
  const details: SearchToolDetails = {
1043
1102
  scopePath,
@@ -1049,7 +1108,11 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
1049
1108
  truncated: false,
1050
1109
  missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
1051
1110
  };
1052
- const text = warningNote ? `No matches found\n${warningNote}` : "No matches found";
1111
+ const skipPastEnd = canPaginate && normalizedSkip > 0 && totalFiles > 0 && skipFiles >= totalFiles;
1112
+ const noMatchText = skipPastEnd
1113
+ ? `No more results (${totalFilesLabel} files total; skip=${normalizedSkip} is past the end)`
1114
+ : "No matches found";
1115
+ const text = warningNote ? `${noMatchText}\n${warningNote}` : noMatchText;
1053
1116
  return toolResult(details).text(text).done();
1054
1117
  }
1055
1118
  const outputLines: string[] = [];
@@ -17,6 +17,8 @@ const SQLITE_PATH_PATTERN = /\.(?:sqlite3?|db3?)(?=(?::|\?|$))/gi;
17
17
  const DEFAULT_QUERY_LIMIT = 20;
18
18
  const DEFAULT_SCHEMA_SAMPLE_LIMIT = 5;
19
19
  const MAX_QUERY_LIMIT = 500;
20
+ /** Row cap for raw `?q=` SQL — protects against `SELECT *` on multi-million-row tables. */
21
+ export const MAX_RAW_QUERY_ROWS = 1000;
20
22
  const MAX_RENDER_WIDTH = 120;
21
23
  const MAX_COLUMN_WIDTH = 40;
22
24
  const MIN_COLUMN_WIDTH = 1;
@@ -659,15 +661,25 @@ export function getRowByRowId(db: Database, table: string, key: string): Record<
659
661
  .get(binding);
660
662
  }
661
663
 
662
- export function executeReadQuery(db: Database, sql: string): { columns: string[]; rows: Record<string, unknown>[] } {
664
+ export function executeReadQuery(
665
+ db: Database,
666
+ sql: string,
667
+ ): { columns: string[]; rows: Record<string, unknown>[]; truncated: boolean } {
663
668
  const statement = db.prepare<SqliteRow, []>(sql);
664
669
  if (statement.paramsCount > 0) {
665
670
  throw new ToolError("SQLite raw queries do not support bound parameters");
666
671
  }
667
- return {
668
- columns: [...statement.columnNames],
669
- rows: statement.all(),
670
- };
672
+ const columns = [...statement.columnNames];
673
+ const rows: SqliteRow[] = [];
674
+ let truncated = false;
675
+ for (const row of statement.iterate()) {
676
+ if (rows.length >= MAX_RAW_QUERY_ROWS) {
677
+ truncated = true;
678
+ break;
679
+ }
680
+ rows.push(row);
681
+ }
682
+ return { columns, rows, truncated };
671
683
  }
672
684
 
673
685
  export function insertRow(db: Database, table: string, data: Record<string, unknown>): void {
package/src/tools/ssh.ts CHANGED
@@ -10,7 +10,7 @@ import type { Theme } from "../modes/theme/theme";
10
10
  import sshDescriptionBase from "../prompts/tools/ssh.md" with { type: "text" };
11
11
  import { DEFAULT_MAX_BYTES, streamTailUpdates, TailBuffer } from "../session/streaming-output";
12
12
  import type { SSHHostInfo } from "../ssh/connection-manager";
13
- import { ensureHostInfo, getHostInfoForHost } from "../ssh/connection-manager";
13
+ import { ensureHostInfo, getCachedHostInfoSync } from "../ssh/connection-manager";
14
14
  import { executeSSH } from "../ssh/ssh-executor";
15
15
  import { renderStatusLine } from "../tui";
16
16
  import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
@@ -33,8 +33,8 @@ export interface SSHToolDetails {
33
33
  meta?: OutputMeta;
34
34
  }
35
35
 
36
- async function formatHostEntry(host: SSHHost): Promise<string> {
37
- const info = await getHostInfoForHost(host);
36
+ function formatHostEntry(host: SSHHost): string {
37
+ const info = getCachedHostInfoSync(host);
38
38
 
39
39
  let shell: string;
40
40
  if (!info) {
@@ -59,12 +59,12 @@ async function formatHostEntry(host: SSHHost): Promise<string> {
59
59
  return `- ${host.name} (${host.host}) | ${shell}`;
60
60
  }
61
61
 
62
- async function formatDescription(hosts: SSHHost[]): Promise<string> {
62
+ function formatDescription(hosts: SSHHost[]): string {
63
63
  const baseDescription = prompt.render(sshDescriptionBase);
64
64
  if (hosts.length === 0) {
65
65
  return baseDescription;
66
66
  }
67
- const hostList = (await Promise.all(hosts.map(formatHostEntry))).join("\n");
67
+ const hostList = hosts.map(formatHostEntry).join("\n");
68
68
  return `${baseDescription}\n\nAvailable hosts:\n${hostList}`;
69
69
  }
70
70
 
@@ -206,7 +206,7 @@ export async function loadSshTool(session: ToolSession): Promise<SshTool | null>
206
206
  const descriptionHosts = hostNames
207
207
  .map(name => hostsByName.get(name))
208
208
  .filter((host): host is SSHHost => host !== undefined);
209
- const description = await formatDescription(descriptionHosts);
209
+ const description = formatDescription(descriptionHosts);
210
210
 
211
211
  return new SshTool(session, hostNames, hostsByName, description);
212
212
  }
@@ -245,7 +245,7 @@ export const sshToolRenderer = {
245
245
  const cmdLines = formatSshCommandLines(command, uiTheme);
246
246
  const outputBlock = new CachedOutputBlock();
247
247
  return markFramedBlockComponent({
248
- render: (width: number): string[] =>
248
+ render: (width: number): readonly string[] =>
249
249
  outputBlock.render(
250
250
  {
251
251
  header,
@@ -282,7 +282,7 @@ export const sshToolRenderer = {
282
282
  const outputBlock = new CachedOutputBlock();
283
283
 
284
284
  return markFramedBlockComponent({
285
- render: (width: number): string[] => {
285
+ render: (width: number): readonly string[] => {
286
286
  // REACTIVE: read mutable options at render time
287
287
  const { expanded, renderContext } = options;
288
288
  // Strip LLM-facing notice so we don't echo it next to the styled warning.
package/src/tools/todo.ts CHANGED
@@ -51,7 +51,7 @@ export interface TodoToolDetails {
51
51
  // =============================================================================
52
52
 
53
53
  const TodoOp = z
54
- .enum(["init", "start", "done", "rm", "drop", "append", "note"] as const)
54
+ .enum(["init", "start", "done", "rm", "drop", "append", "note", "view"] as const)
55
55
  .describe("operation to apply");
56
56
 
57
57
  const InitListEntry = z.object({
@@ -285,6 +285,22 @@ function initPhases(entry: TodoOpEntryValue, errors: string[]): TodoPhase[] {
285
285
  errors.push("Missing list for init operation");
286
286
  return [];
287
287
  }
288
+ // Duplicate phase names / task contents would be permanently unaddressable
289
+ // (every targeting op resolves the first match), so reject them up front.
290
+ const seenPhases = new Set<string>();
291
+ const seenTasks = new Set<string>();
292
+ for (const listEntry of entry.list) {
293
+ if (seenPhases.has(listEntry.phase)) {
294
+ errors.push(`Duplicate phase "${listEntry.phase}" in init list`);
295
+ }
296
+ seenPhases.add(listEntry.phase);
297
+ for (const content of listEntry.items) {
298
+ if (seenTasks.has(content)) {
299
+ errors.push(`Duplicate task "${content}" in init list`);
300
+ }
301
+ seenTasks.add(content);
302
+ }
303
+ }
288
304
  return entry.list.map(listEntry => ({
289
305
  name: listEntry.phase,
290
306
  tasks: listEntry.items.map<TodoItem>(content => ({ content, status: "pending" })),
@@ -301,6 +317,19 @@ function appendItems(phases: TodoPhase[], entry: TodoOpEntryValue, errors: strin
301
317
  return phases;
302
318
  }
303
319
 
320
+ // Validate the whole batch before mutating so a failing op reports every
321
+ // duplicate and leaves nothing half-applied.
322
+ const seen = new Set<string>();
323
+ let hasDuplicate = false;
324
+ for (const content of entry.items) {
325
+ if (seen.has(content) || findTaskByContent(phases, content)) {
326
+ errors.push(`Task "${content}" already exists`);
327
+ hasDuplicate = true;
328
+ }
329
+ seen.add(content);
330
+ }
331
+ if (hasDuplicate) return phases;
332
+
304
333
  let phase = findPhaseByName(phases, entry.phase);
305
334
  if (!phase) {
306
335
  phase = { name: entry.phase, tasks: [] };
@@ -308,10 +337,6 @@ function appendItems(phases: TodoPhase[], entry: TodoOpEntryValue, errors: strin
308
337
  }
309
338
 
310
339
  for (const content of entry.items) {
311
- if (findTaskByContent(phases, content)) {
312
- errors.push(`Task "${content}" already exists`);
313
- return phases;
314
- }
315
340
  phase.tasks.push({ content, status: "pending" });
316
341
  }
317
342
  return phases;
@@ -380,6 +405,8 @@ function applyEntry(phases: TodoPhase[], entry: TodoOpEntryValue, errors: string
380
405
  }
381
406
  case "append":
382
407
  return appendItems(phases, entry, errors);
408
+ case "view":
409
+ return phases;
383
410
  }
384
411
  }
385
412
 
@@ -523,9 +550,12 @@ export function markdownToPhases(md: string): { phases: TodoPhase[]; errors: str
523
550
  return { phases, errors };
524
551
  }
525
552
 
526
- function formatSummary(phases: TodoPhase[], errors: string[]): string {
553
+ function formatSummary(phases: TodoPhase[], errors: string[], readOnly = false): string {
527
554
  const tasks = phases.flatMap(phase => phase.tasks);
528
- if (tasks.length === 0) return errors.length > 0 ? `Errors: ${errors.join("; ")}` : "Todo list cleared.";
555
+ if (tasks.length === 0) {
556
+ if (errors.length > 0) return `Errors: ${errors.join("; ")}`;
557
+ return readOnly ? "Todo list is empty." : "Todo list cleared.";
558
+ }
529
559
 
530
560
  const remainingByPhase = phases
531
561
  .map(phase => ({
@@ -608,15 +638,24 @@ export class TodoTool implements AgentTool<typeof todoSchema, TodoToolDetails> {
608
638
  _context?: AgentToolContext,
609
639
  ): Promise<AgentToolResult<TodoToolDetails>> {
610
640
  const previousPhases = clonePhases(this.session.getTodoPhases?.() ?? []);
611
- const { phases: updated, errors } = applyParams(clonePhases(previousPhases), params);
612
- const completedTasks = getCompletionTransitions(previousPhases, updated);
613
- this.session.setTodoPhases?.(updated);
641
+ // Pure-view calls are reads: no normalization, no state write.
642
+ const readOnly = params.ops.every(entry => entry.op === "view");
643
+ const { phases: updated, errors } = readOnly
644
+ ? { phases: previousPhases, errors: [] as string[] }
645
+ : applyParams(clonePhases(previousPhases), params);
646
+ // A batch with any error is discarded wholesale: persisting a
647
+ // half-applied batch makes the natural retry hit "already exists" for
648
+ // the ops that did land. State and rendered summary stay at previous.
649
+ const failed = errors.length > 0;
650
+ const effective = failed ? previousPhases : updated;
651
+ const completedTasks = readOnly || failed ? [] : getCompletionTransitions(previousPhases, updated);
652
+ if (!readOnly && !failed) this.session.setTodoPhases?.(updated);
614
653
  const storage = this.session.getSessionFile() ? "session" : "memory";
615
- const details: TodoToolDetails = { phases: updated, storage };
654
+ const details: TodoToolDetails = { phases: effective, storage };
616
655
  if (completedTasks.length > 0) details.completedTasks = completedTasks;
617
656
 
618
657
  return {
619
- content: [{ type: "text", text: formatSummary(updated, errors) }],
658
+ content: [{ type: "text", text: formatSummary(effective, errors, readOnly) }],
620
659
  details,
621
660
  isError: errors.length > 0 ? true : undefined,
622
661
  };