@oh-my-pi/pi-coding-agent 8.0.20 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (421) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/docs/session.md +111 -46
  3. package/examples/custom-tools/hello/index.ts +1 -1
  4. package/examples/custom-tools/todo/index.ts +3 -4
  5. package/examples/extensions/api-demo.ts +0 -1
  6. package/examples/extensions/chalk-logger.ts +2 -3
  7. package/examples/extensions/hello.ts +0 -1
  8. package/examples/extensions/pirate.ts +0 -1
  9. package/examples/extensions/plan-mode.ts +15 -16
  10. package/examples/extensions/todo.ts +3 -4
  11. package/examples/extensions/tools.ts +1 -2
  12. package/examples/extensions/with-deps/index.ts +0 -1
  13. package/examples/hooks/auto-commit-on-exit.ts +1 -2
  14. package/examples/hooks/confirm-destructive.ts +0 -1
  15. package/examples/hooks/custom-compaction.ts +1 -2
  16. package/examples/hooks/dirty-repo-guard.ts +0 -1
  17. package/examples/hooks/file-trigger.ts +3 -4
  18. package/examples/hooks/git-checkpoint.ts +0 -1
  19. package/examples/hooks/handoff.ts +3 -4
  20. package/examples/hooks/permission-gate.ts +1 -2
  21. package/examples/hooks/protected-paths.ts +1 -2
  22. package/examples/hooks/qna.ts +2 -3
  23. package/examples/hooks/snake.ts +4 -5
  24. package/examples/hooks/status-line.ts +0 -1
  25. package/examples/sdk/01-minimal.ts +2 -3
  26. package/examples/sdk/02-custom-model.ts +2 -3
  27. package/examples/sdk/03-custom-prompt.ts +3 -4
  28. package/examples/sdk/04-skills.ts +2 -3
  29. package/examples/sdk/06-extensions.ts +1 -2
  30. package/examples/sdk/06-hooks.ts +6 -7
  31. package/examples/sdk/07-context-files.ts +0 -1
  32. package/examples/sdk/08-prompt-templates.ts +0 -1
  33. package/examples/sdk/08-slash-commands.ts +0 -1
  34. package/examples/sdk/09-api-keys-and-oauth.ts +0 -1
  35. package/examples/sdk/10-settings.ts +0 -1
  36. package/examples/sdk/11-sessions.ts +0 -1
  37. package/package.json +54 -23
  38. package/scripts/format-prompts.ts +0 -1
  39. package/src/capability/context-file.ts +3 -4
  40. package/src/capability/extension-module.ts +3 -4
  41. package/src/capability/extension.ts +3 -4
  42. package/src/capability/fs.ts +20 -21
  43. package/src/capability/hook.ts +3 -4
  44. package/src/capability/index.ts +15 -16
  45. package/src/capability/instruction.ts +3 -4
  46. package/src/capability/mcp.ts +3 -4
  47. package/src/capability/prompt.ts +3 -4
  48. package/src/capability/rule.ts +3 -4
  49. package/src/capability/settings.ts +2 -3
  50. package/src/capability/skill.ts +3 -4
  51. package/src/capability/slash-command.ts +3 -4
  52. package/src/capability/ssh.ts +3 -4
  53. package/src/capability/system-prompt.ts +3 -4
  54. package/src/capability/tool.ts +3 -4
  55. package/src/cli/args.ts +5 -6
  56. package/src/cli/config-cli.ts +6 -7
  57. package/src/cli/file-processor.ts +19 -17
  58. package/src/cli/jupyter-cli.ts +105 -0
  59. package/src/cli/list-models.ts +10 -11
  60. package/src/cli/plugin-cli.ts +20 -25
  61. package/src/cli/session-picker.ts +2 -3
  62. package/src/cli/setup-cli.ts +2 -3
  63. package/src/cli/stats-cli.ts +2 -3
  64. package/src/cli/update-cli.ts +25 -22
  65. package/src/commit/agentic/agent.ts +307 -0
  66. package/src/commit/agentic/fallback.ts +96 -0
  67. package/src/commit/agentic/index.ts +351 -0
  68. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  69. package/src/commit/agentic/prompts/session-user.md +26 -0
  70. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  71. package/src/commit/agentic/prompts/system.md +40 -0
  72. package/src/commit/agentic/state.ts +69 -0
  73. package/src/commit/agentic/tools/analyze-file.ts +131 -0
  74. package/src/commit/agentic/tools/git-file-diff.ts +194 -0
  75. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  76. package/src/commit/agentic/tools/git-overview.ts +84 -0
  77. package/src/commit/agentic/tools/index.ts +56 -0
  78. package/src/commit/agentic/tools/propose-changelog.ts +128 -0
  79. package/src/commit/agentic/tools/propose-commit.ts +154 -0
  80. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  81. package/src/commit/agentic/tools/split-commit.ts +280 -0
  82. package/src/commit/agentic/topo-sort.ts +44 -0
  83. package/src/commit/agentic/trivial.ts +51 -0
  84. package/src/commit/agentic/validation.ts +200 -0
  85. package/src/commit/analysis/conventional.ts +165 -0
  86. package/src/commit/analysis/index.ts +4 -0
  87. package/src/commit/analysis/scope.ts +242 -0
  88. package/src/commit/analysis/summary.ts +112 -0
  89. package/src/commit/analysis/validation.ts +66 -0
  90. package/src/commit/changelog/detect.ts +36 -0
  91. package/src/commit/changelog/generate.ts +110 -0
  92. package/src/commit/changelog/index.ts +233 -0
  93. package/src/commit/changelog/parse.ts +44 -0
  94. package/src/commit/cli.ts +93 -0
  95. package/src/commit/git/diff.ts +148 -0
  96. package/src/commit/git/errors.ts +11 -0
  97. package/src/commit/git/index.ts +212 -0
  98. package/src/commit/git/operations.ts +53 -0
  99. package/src/commit/index.ts +5 -0
  100. package/src/commit/map-reduce/index.ts +63 -0
  101. package/src/commit/map-reduce/map-phase.ts +178 -0
  102. package/src/commit/map-reduce/reduce-phase.ts +145 -0
  103. package/src/commit/map-reduce/utils.ts +9 -0
  104. package/src/commit/message.ts +11 -0
  105. package/src/commit/model-selection.ts +80 -0
  106. package/src/commit/pipeline.ts +240 -0
  107. package/src/commit/prompts/analysis-system.md +155 -0
  108. package/src/commit/prompts/analysis-user.md +41 -0
  109. package/src/commit/prompts/changelog-system.md +56 -0
  110. package/src/commit/prompts/changelog-user.md +19 -0
  111. package/src/commit/prompts/file-observer-system.md +26 -0
  112. package/src/commit/prompts/file-observer-user.md +9 -0
  113. package/src/commit/prompts/reduce-system.md +60 -0
  114. package/src/commit/prompts/reduce-user.md +17 -0
  115. package/src/commit/prompts/summary-retry.md +4 -0
  116. package/src/commit/prompts/summary-system.md +52 -0
  117. package/src/commit/prompts/summary-user.md +13 -0
  118. package/src/commit/prompts/types-description.md +2 -0
  119. package/src/commit/types.ts +109 -0
  120. package/src/commit/utils/exclusions.ts +42 -0
  121. package/src/config/file-lock.ts +121 -0
  122. package/src/config/keybindings.ts +6 -8
  123. package/src/config/model-registry.ts +65 -38
  124. package/src/config/model-resolver.ts +18 -19
  125. package/src/config/prompt-templates.ts +11 -11
  126. package/src/config/settings-manager.ts +141 -50
  127. package/src/config.ts +64 -66
  128. package/src/cursor.ts +11 -9
  129. package/src/discovery/agents-md.ts +11 -12
  130. package/src/discovery/builtin.ts +68 -73
  131. package/src/discovery/claude.ts +41 -42
  132. package/src/discovery/cline.ts +11 -12
  133. package/src/discovery/codex.ts +52 -53
  134. package/src/discovery/cursor.ts +9 -10
  135. package/src/discovery/gemini.ts +17 -22
  136. package/src/discovery/github.ts +13 -14
  137. package/src/discovery/helpers.ts +35 -34
  138. package/src/discovery/index.ts +22 -24
  139. package/src/discovery/mcp-json.ts +8 -9
  140. package/src/discovery/ssh.ts +8 -9
  141. package/src/discovery/vscode.ts +4 -5
  142. package/src/discovery/windsurf.ts +6 -7
  143. package/src/exa/company.ts +1 -2
  144. package/src/exa/index.ts +2 -3
  145. package/src/exa/linkedin.ts +1 -2
  146. package/src/exa/mcp-client.ts +14 -16
  147. package/src/exa/render.ts +10 -11
  148. package/src/exa/researcher.ts +1 -2
  149. package/src/exa/search.ts +1 -2
  150. package/src/exa/types.ts +0 -1
  151. package/src/exa/websets.ts +1 -2
  152. package/src/exec/bash-executor.ts +3 -4
  153. package/src/exec/exec.ts +0 -1
  154. package/src/export/custom-share.ts +5 -6
  155. package/src/export/html/index.ts +24 -21
  156. package/src/export/ttsr.ts +2 -3
  157. package/src/extensibility/custom-commands/bundled/review/index.ts +7 -8
  158. package/src/extensibility/custom-commands/loader.ts +18 -15
  159. package/src/extensibility/custom-commands/types.ts +2 -3
  160. package/src/extensibility/custom-tools/loader.ts +11 -12
  161. package/src/extensibility/custom-tools/types.ts +7 -8
  162. package/src/extensibility/custom-tools/wrapper.ts +2 -3
  163. package/src/extensibility/extensions/loader.ts +76 -54
  164. package/src/extensibility/extensions/runner.ts +11 -12
  165. package/src/extensibility/extensions/types.ts +20 -27
  166. package/src/extensibility/extensions/wrapper.ts +3 -4
  167. package/src/extensibility/hooks/index.ts +1 -1
  168. package/src/extensibility/hooks/loader.ts +9 -10
  169. package/src/extensibility/hooks/runner.ts +7 -8
  170. package/src/extensibility/hooks/tool-wrapper.ts +0 -1
  171. package/src/extensibility/hooks/types.ts +11 -18
  172. package/src/extensibility/plugins/doctor.ts +3 -3
  173. package/src/extensibility/plugins/installer.ts +27 -27
  174. package/src/extensibility/plugins/loader.ts +59 -56
  175. package/src/extensibility/plugins/manager.ts +211 -171
  176. package/src/extensibility/plugins/parser.ts +1 -1
  177. package/src/extensibility/plugins/paths.ts +8 -8
  178. package/src/extensibility/skills.ts +63 -60
  179. package/src/extensibility/slash-commands.ts +10 -10
  180. package/src/index.ts +54 -54
  181. package/src/internal-urls/agent-protocol.ts +21 -11
  182. package/src/internal-urls/artifact-protocol.ts +17 -13
  183. package/src/internal-urls/router.ts +1 -2
  184. package/src/internal-urls/rule-protocol.ts +3 -4
  185. package/src/internal-urls/skill-protocol.ts +3 -4
  186. package/src/ipy/executor.ts +109 -9
  187. package/src/ipy/gateway-coordinator.ts +79 -90
  188. package/src/ipy/kernel.ts +32 -30
  189. package/src/ipy/modules.ts +13 -13
  190. package/src/lsp/client.ts +21 -10
  191. package/src/lsp/clients/biome-client.ts +1 -2
  192. package/src/lsp/clients/index.ts +3 -3
  193. package/src/lsp/clients/lsp-linter-client.ts +4 -5
  194. package/src/lsp/config.ts +15 -15
  195. package/src/lsp/edits.ts +4 -5
  196. package/src/lsp/index.ts +43 -44
  197. package/src/lsp/lspmux.ts +8 -8
  198. package/src/lsp/render.ts +99 -61
  199. package/src/lsp/utils.ts +3 -3
  200. package/src/main.ts +71 -37
  201. package/src/mcp/client.ts +2 -3
  202. package/src/mcp/config.ts +5 -6
  203. package/src/mcp/json-rpc.ts +0 -1
  204. package/src/mcp/loader.ts +6 -7
  205. package/src/mcp/manager.ts +17 -18
  206. package/src/mcp/tool-bridge.ts +4 -9
  207. package/src/mcp/tool-cache.ts +2 -3
  208. package/src/mcp/transports/http.ts +2 -4
  209. package/src/mcp/transports/stdio.ts +1 -2
  210. package/src/migrations.ts +63 -52
  211. package/src/modes/components/armin.ts +4 -5
  212. package/src/modes/components/assistant-message.ts +33 -5
  213. package/src/modes/components/bash-execution.ts +7 -8
  214. package/src/modes/components/bordered-loader.ts +3 -3
  215. package/src/modes/components/branch-summary-message.ts +3 -3
  216. package/src/modes/components/compaction-summary-message.ts +3 -3
  217. package/src/modes/components/countdown-timer.ts +0 -1
  218. package/src/modes/components/custom-message.ts +5 -5
  219. package/src/modes/components/diff.ts +1 -1
  220. package/src/modes/components/dynamic-border.ts +2 -2
  221. package/src/modes/components/extensions/extension-dashboard.ts +6 -7
  222. package/src/modes/components/extensions/extension-list.ts +2 -3
  223. package/src/modes/components/extensions/inspector-panel.ts +3 -4
  224. package/src/modes/components/extensions/state-manager.ts +25 -26
  225. package/src/modes/components/extensions/types.ts +1 -2
  226. package/src/modes/components/footer.ts +47 -43
  227. package/src/modes/components/history-search.ts +2 -2
  228. package/src/modes/components/hook-editor.ts +3 -4
  229. package/src/modes/components/hook-input.ts +2 -3
  230. package/src/modes/components/hook-message.ts +5 -5
  231. package/src/modes/components/hook-selector.ts +2 -3
  232. package/src/modes/components/keybinding-hints.ts +2 -3
  233. package/src/modes/components/login-dialog.ts +2 -2
  234. package/src/modes/components/model-selector.ts +12 -12
  235. package/src/modes/components/oauth-selector.ts +2 -2
  236. package/src/modes/components/plugin-settings.ts +20 -20
  237. package/src/modes/components/python-execution.ts +7 -8
  238. package/src/modes/components/queue-mode-selector.ts +3 -3
  239. package/src/modes/components/read-tool-group.ts +2 -2
  240. package/src/modes/components/session-selector.ts +4 -4
  241. package/src/modes/components/settings-defs.ts +77 -69
  242. package/src/modes/components/settings-selector.ts +16 -16
  243. package/src/modes/components/show-images-selector.ts +2 -2
  244. package/src/modes/components/status-line/segments.ts +4 -4
  245. package/src/modes/components/status-line/separators.ts +1 -1
  246. package/src/modes/components/status-line/types.ts +2 -2
  247. package/src/modes/components/status-line-segment-editor.ts +7 -8
  248. package/src/modes/components/status-line.ts +12 -12
  249. package/src/modes/components/theme-selector.ts +8 -7
  250. package/src/modes/components/thinking-selector.ts +4 -4
  251. package/src/modes/components/todo-display.ts +2 -2
  252. package/src/modes/components/todo-reminder.ts +4 -4
  253. package/src/modes/components/tool-execution.ts +16 -19
  254. package/src/modes/components/tree-selector.ts +12 -12
  255. package/src/modes/components/ttsr-notification.ts +5 -5
  256. package/src/modes/components/user-message-selector.ts +1 -1
  257. package/src/modes/components/user-message.ts +1 -1
  258. package/src/modes/components/visual-truncate.ts +0 -1
  259. package/src/modes/components/welcome.ts +4 -4
  260. package/src/modes/controllers/command-controller.ts +46 -47
  261. package/src/modes/controllers/event-controller.ts +16 -20
  262. package/src/modes/controllers/extension-ui-controller.ts +40 -46
  263. package/src/modes/controllers/input-controller.ts +17 -18
  264. package/src/modes/controllers/selector-controller.ts +103 -91
  265. package/src/modes/index.ts +3 -3
  266. package/src/modes/interactive-mode.ts +31 -31
  267. package/src/modes/print-mode.ts +12 -13
  268. package/src/modes/rpc/rpc-client.ts +7 -8
  269. package/src/modes/rpc/rpc-mode.ts +24 -28
  270. package/src/modes/rpc/rpc-types.ts +3 -4
  271. package/src/modes/theme/mermaid-cache.ts +89 -0
  272. package/src/modes/theme/theme.ts +130 -53
  273. package/src/modes/types.ts +10 -10
  274. package/src/modes/utils/ui-helpers.ts +17 -17
  275. package/src/patch/applicator.ts +18 -19
  276. package/src/patch/diff.ts +1 -2
  277. package/src/patch/fuzzy.ts +1 -2
  278. package/src/patch/index.ts +11 -18
  279. package/src/patch/normalize.ts +4 -4
  280. package/src/patch/normative.ts +1 -2
  281. package/src/patch/parser.ts +8 -9
  282. package/src/patch/shared.ts +43 -16
  283. package/src/prompts/tools/task.md +2 -0
  284. package/src/sdk.ts +100 -65
  285. package/src/session/agent-session.ts +84 -85
  286. package/src/session/agent-storage.ts +43 -39
  287. package/src/session/artifacts.ts +32 -10
  288. package/src/session/auth-storage.ts +50 -39
  289. package/src/session/compaction/branch-summarization.ts +7 -10
  290. package/src/session/compaction/compaction.ts +8 -19
  291. package/src/session/compaction/utils.ts +6 -9
  292. package/src/session/history-storage.ts +10 -10
  293. package/src/session/messages.ts +4 -5
  294. package/src/session/session-manager.ts +76 -65
  295. package/src/session/session-storage.ts +57 -69
  296. package/src/session/storage-migration.ts +14 -56
  297. package/src/session/streaming-output.ts +2 -2
  298. package/src/ssh/connection-manager.ts +43 -50
  299. package/src/ssh/ssh-executor.ts +2 -2
  300. package/src/ssh/sshfs-mount.ts +11 -18
  301. package/src/system-prompt.ts +28 -35
  302. package/src/task/agents.ts +45 -30
  303. package/src/task/commands.ts +6 -7
  304. package/src/task/discovery.ts +39 -76
  305. package/src/task/executor.ts +14 -15
  306. package/src/task/index.ts +40 -34
  307. package/src/task/output-manager.ts +93 -0
  308. package/src/task/parallel.ts +0 -1
  309. package/src/task/render.ts +24 -30
  310. package/src/task/subprocess-tool-registry.ts +1 -2
  311. package/src/task/worker-protocol.ts +3 -3
  312. package/src/task/worker.ts +33 -39
  313. package/src/task/worktree.ts +19 -19
  314. package/src/tools/ask.ts +41 -20
  315. package/src/tools/bash-interceptor.ts +1 -5
  316. package/src/tools/bash.ts +91 -97
  317. package/src/tools/calculator.ts +49 -47
  318. package/src/tools/complete.ts +4 -5
  319. package/src/tools/context.ts +2 -2
  320. package/src/tools/fetch.ts +84 -124
  321. package/src/tools/find.ts +94 -98
  322. package/src/tools/gemini-image.ts +14 -14
  323. package/src/tools/grep.ts +100 -116
  324. package/src/tools/index.ts +80 -55
  325. package/src/tools/list-limit.ts +1 -1
  326. package/src/tools/ls.ts +44 -70
  327. package/src/tools/notebook.ts +51 -67
  328. package/src/tools/output-meta.ts +3 -4
  329. package/src/tools/output-utils.ts +2 -2
  330. package/src/tools/path-utils.ts +5 -5
  331. package/src/tools/python.ts +104 -217
  332. package/src/tools/read.ts +92 -33
  333. package/src/tools/render-utils.ts +8 -23
  334. package/src/tools/renderers.ts +6 -7
  335. package/src/tools/review.ts +8 -11
  336. package/src/tools/ssh.ts +69 -49
  337. package/src/tools/todo-write.ts +37 -25
  338. package/src/tools/tool-errors.ts +3 -3
  339. package/src/tools/tool-result.ts +3 -8
  340. package/src/tools/write.ts +99 -75
  341. package/src/tui/code-cell.ts +109 -0
  342. package/src/tui/file-list.ts +47 -0
  343. package/src/tui/index.ts +11 -0
  344. package/src/tui/output-block.ts +72 -0
  345. package/src/tui/status-line.ts +39 -0
  346. package/src/tui/tree-list.ts +55 -0
  347. package/src/tui/types.ts +16 -0
  348. package/src/tui/utils.ts +48 -0
  349. package/src/utils/changelog.ts +9 -10
  350. package/src/utils/clipboard.ts +11 -11
  351. package/src/utils/file-mentions.ts +4 -10
  352. package/src/utils/frontmatter.ts +6 -3
  353. package/src/utils/fuzzy.ts +2 -2
  354. package/src/utils/image-convert.ts +1 -1
  355. package/src/utils/image-resize.ts +1 -1
  356. package/src/utils/mime.ts +2 -2
  357. package/src/utils/shell-snapshot.ts +11 -13
  358. package/src/utils/shell.ts +4 -5
  359. package/src/utils/title-generator.ts +8 -9
  360. package/src/utils/tools-manager.ts +23 -23
  361. package/src/vendor/photon/index.js +1099 -1059
  362. package/src/vendor/photon/photon_rs_bg.wasm +0 -0
  363. package/src/web/scrapers/artifacthub.ts +1 -1
  364. package/src/web/scrapers/arxiv.ts +2 -2
  365. package/src/web/scrapers/bluesky.ts +2 -2
  366. package/src/web/scrapers/cheatsh.ts +1 -1
  367. package/src/web/scrapers/chocolatey.ts +2 -2
  368. package/src/web/scrapers/choosealicense.ts +5 -5
  369. package/src/web/scrapers/cisa-kev.ts +1 -1
  370. package/src/web/scrapers/crossref.ts +2 -2
  371. package/src/web/scrapers/devto.ts +3 -3
  372. package/src/web/scrapers/discogs.ts +3 -4
  373. package/src/web/scrapers/discourse.ts +1 -1
  374. package/src/web/scrapers/dockerhub.ts +1 -1
  375. package/src/web/scrapers/fdroid.ts +2 -2
  376. package/src/web/scrapers/firefox-addons.ts +3 -3
  377. package/src/web/scrapers/flathub.ts +1 -1
  378. package/src/web/scrapers/github.ts +3 -3
  379. package/src/web/scrapers/gitlab.ts +4 -4
  380. package/src/web/scrapers/hackernews.ts +2 -2
  381. package/src/web/scrapers/huggingface.ts +1 -1
  382. package/src/web/scrapers/iacr.ts +2 -2
  383. package/src/web/scrapers/index.ts +0 -1
  384. package/src/web/scrapers/jetbrains-marketplace.ts +1 -1
  385. package/src/web/scrapers/lemmy.ts +2 -2
  386. package/src/web/scrapers/maven.ts +2 -2
  387. package/src/web/scrapers/mdn.ts +2 -4
  388. package/src/web/scrapers/metacpan.ts +2 -2
  389. package/src/web/scrapers/musicbrainz.ts +1 -2
  390. package/src/web/scrapers/npm.ts +1 -1
  391. package/src/web/scrapers/nuget.ts +2 -2
  392. package/src/web/scrapers/nvd.ts +3 -3
  393. package/src/web/scrapers/ollama.ts +7 -9
  394. package/src/web/scrapers/opencorporates.ts +2 -2
  395. package/src/web/scrapers/openlibrary.ts +6 -6
  396. package/src/web/scrapers/orcid.ts +0 -1
  397. package/src/web/scrapers/osv.ts +2 -2
  398. package/src/web/scrapers/packagist.ts +1 -1
  399. package/src/web/scrapers/pubmed.ts +1 -2
  400. package/src/web/scrapers/rawg.ts +2 -2
  401. package/src/web/scrapers/readthedocs.ts +1 -2
  402. package/src/web/scrapers/repology.ts +2 -2
  403. package/src/web/scrapers/rfc.ts +1 -1
  404. package/src/web/scrapers/searchcode.ts +2 -2
  405. package/src/web/scrapers/semantic-scholar.ts +1 -1
  406. package/src/web/scrapers/snapcraft.ts +2 -2
  407. package/src/web/scrapers/sourcegraph.ts +1 -1
  408. package/src/web/scrapers/spdx.ts +3 -3
  409. package/src/web/scrapers/spotify.ts +0 -1
  410. package/src/web/scrapers/twitter.ts +1 -1
  411. package/src/web/scrapers/types.ts +1 -2
  412. package/src/web/scrapers/utils.ts +5 -5
  413. package/src/web/scrapers/wikidata.ts +3 -3
  414. package/src/web/scrapers/youtube.ts +9 -14
  415. package/src/web/search/auth.ts +5 -10
  416. package/src/web/search/index.ts +11 -21
  417. package/src/web/search/providers/anthropic.ts +3 -9
  418. package/src/web/search/providers/exa.ts +6 -10
  419. package/src/web/search/providers/perplexity.ts +5 -5
  420. package/src/web/search/render.ts +129 -175
  421. package/tsconfig.json +0 -42
@@ -1,26 +1,27 @@
1
- import { mkdir, rm } from "node:fs/promises";
1
+ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
- import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
5
- import type { RenderResultOptions } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
6
- import { type Theme, theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
7
- import fetchDescription from "@oh-my-pi/pi-coding-agent/prompts/tools/fetch.md" with { type: "text" };
8
- import type { OutputMeta } from "@oh-my-pi/pi-coding-agent/tools/output-meta";
9
- import { ToolAbortError } from "@oh-my-pi/pi-coding-agent/tools/tool-errors";
10
- import { ensureTool } from "@oh-my-pi/pi-coding-agent/utils/tools-manager";
11
- import { specialHandlers } from "@oh-my-pi/pi-coding-agent/web/scrapers/index";
12
- import type { RenderResult } from "@oh-my-pi/pi-coding-agent/web/scrapers/types";
13
- import { finalizeOutput, loadPage, MAX_OUTPUT_CHARS } from "@oh-my-pi/pi-coding-agent/web/scrapers/types";
14
- import { convertWithMarkitdown, fetchBinary } from "@oh-my-pi/pi-coding-agent/web/scrapers/utils";
15
4
  import type { Component } from "@oh-my-pi/pi-tui";
16
5
  import { Text } from "@oh-my-pi/pi-tui";
17
6
  import { ptree } from "@oh-my-pi/pi-utils";
18
7
  import { type Static, Type } from "@sinclair/typebox";
19
8
  import { nanoid } from "nanoid";
20
9
  import { parse as parseHtml } from "node-html-parser";
21
- import type { ToolSession } from "./index";
10
+ import { renderPromptTemplate } from "../config/prompt-templates";
11
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
12
+ import { type Theme, theme } from "../modes/theme/theme";
13
+ import fetchDescription from "../prompts/tools/fetch.md" with { type: "text" };
14
+ import { renderOutputBlock, renderStatusLine } from "../tui";
15
+ import { ensureTool } from "../utils/tools-manager";
16
+ import { specialHandlers } from "../web/scrapers";
17
+ import type { RenderResult } from "../web/scrapers/types";
18
+ import { finalizeOutput, loadPage, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
19
+ import { convertWithMarkitdown, fetchBinary } from "../web/scrapers/utils";
20
+ import type { ToolSession } from ".";
22
21
  import { applyListLimit } from "./list-limit";
22
+ import type { OutputMeta } from "./output-meta";
23
23
  import { formatExpandHint } from "./render-utils";
24
+ import { ToolAbortError } from "./tool-errors";
24
25
  import { toolResult } from "./tool-result";
25
26
 
26
27
  // =============================================================================
@@ -449,7 +450,6 @@ async function renderHtmlToText(
449
450
  timeout: number,
450
451
  scratchDir: string,
451
452
  ): Promise<{ content: string; ok: boolean; method: string }> {
452
- await mkdir(scratchDir, { recursive: true });
453
453
  const tmpFile = path.join(scratchDir, `omp-${nanoid()}.html`);
454
454
 
455
455
  try {
@@ -478,7 +478,7 @@ async function renderHtmlToText(
478
478
  return { content: "", ok: false, method: "none" };
479
479
  } finally {
480
480
  try {
481
- await rm(tmpFile, { force: true });
481
+ await fs.rm(tmpFile, { force: true });
482
482
  } catch {}
483
483
  }
484
484
  }
@@ -497,13 +497,13 @@ function isLowQualityOutput(content: string): boolean {
497
497
  "please enable javascript",
498
498
  "browser not supported",
499
499
  ];
500
- if (content.length < 1024 && jsGated.some((t) => lower.includes(t))) {
500
+ if (content.length < 1024 && jsGated.some(t => lower.includes(t))) {
501
501
  return true;
502
502
  }
503
503
 
504
504
  // Mostly navigation (high link/menu density)
505
- const lines = content.split("\n").filter((l) => l.trim());
506
- const shortLines = lines.filter((l) => l.trim().length < 40);
505
+ const lines = content.split("\n").filter(l => l.trim());
506
+ const shortLines = lines.filter(l => l.trim().length < 40);
507
507
  if (lines.length > 10 && shortLines.length / lines.length > 0.7) {
508
508
  return true;
509
509
  }
@@ -694,7 +694,7 @@ async function renderUrl(
694
694
  if (isHtml && !raw) {
695
695
  // 5A: Check for page-specific markdown alternate
696
696
  const alternates = parseAlternateLinks(rawContent, finalUrl);
697
- const markdownAlt = alternates.find((alt) => alt.endsWith(".md") || alt.includes("markdown"));
697
+ const markdownAlt = alternates.find(alt => alt.endsWith(".md") || alt.includes("markdown"));
698
698
  if (markdownAlt) {
699
699
  const resolved = markdownAlt.startsWith("http") ? markdownAlt : new URL(markdownAlt, finalUrl).href;
700
700
  const altResult = await loadPage(resolved, { timeout, signal });
@@ -766,7 +766,7 @@ async function renderUrl(
766
766
  }
767
767
 
768
768
  // 5E: Check for feed alternates
769
- const feedAlternates = alternates.filter((alt) => !alt.endsWith(".md") && !alt.includes("markdown"));
769
+ const feedAlternates = alternates.filter(alt => !alt.endsWith(".md") && !alt.includes("markdown"));
770
770
  for (const altUrl of feedAlternates.slice(0, 2)) {
771
771
  const resolved = altUrl.startsWith("http") ? altUrl : new URL(altUrl, finalUrl).href;
772
772
  const altResult = await loadPage(resolved, { timeout, signal });
@@ -983,7 +983,7 @@ function getDomain(url: string): string {
983
983
 
984
984
  /** Count non-empty lines */
985
985
  function countNonEmptyLines(text: string): number {
986
- return text.split("\n").filter((l) => l.trim()).length;
986
+ return text.split("\n").filter(l => l.trim()).length;
987
987
  }
988
988
 
989
989
  /** Render fetch call (URL preview) */
@@ -993,8 +993,11 @@ export function renderFetchCall(
993
993
  ): Component {
994
994
  const domain = getDomain(args.url);
995
995
  const path = truncate(args.url.replace(/^https?:\/\/[^/]+/, ""), 50, uiTheme.format.ellipsis);
996
- const icon = uiTheme.styledSymbol("status.pending", "muted");
997
- const text = `${icon} ${uiTheme.fg("toolTitle", "Fetch")} ${uiTheme.fg("accent", domain)}${uiTheme.fg("dim", path)}`;
996
+ const description = `${domain}${path ? ` ${path}` : ""}`.trim();
997
+ const meta: string[] = [];
998
+ if (args.raw) meta.push("raw");
999
+ if (args.timeout !== undefined) meta.push(`timeout:${args.timeout}s`);
1000
+ const text = renderStatusLine({ icon: "pending", title: "Fetch", description, meta }, uiTheme);
998
1001
  return new Text(text, 0, 0);
999
1002
  }
1000
1003
 
@@ -1012,125 +1015,82 @@ export function renderFetchResult(
1012
1015
  }
1013
1016
 
1014
1017
  const domain = getDomain(details.finalUrl);
1018
+ const path = truncate(details.finalUrl.replace(/^https?:\/\/[^/]+/, ""), 50, uiTheme.format.ellipsis);
1015
1019
  const hasRedirect = details.url !== details.finalUrl;
1016
1020
  const hasNotes = details.notes.length > 0;
1017
1021
  const truncation = details.meta?.truncation;
1018
1022
  const truncated = Boolean(details.truncated || truncation);
1019
- const statusIcon = truncated
1020
- ? uiTheme.styledSymbol("status.warning", "warning")
1021
- : uiTheme.styledSymbol("status.success", "success");
1022
- const expandHint = formatExpandHint(uiTheme, expanded);
1023
- const expandSuffix = expandHint ? ` ${expandHint}` : "";
1024
- let text = `${statusIcon} ${uiTheme.fg("accent", `(${domain})`)}${uiTheme.sep.dot}${uiTheme.fg("dim", details.method)}${expandSuffix}`;
1025
-
1026
- // Get content text
1023
+
1024
+ const header = renderStatusLine(
1025
+ {
1026
+ icon: truncated ? "warning" : "success",
1027
+ title: "Fetch",
1028
+ description: `${domain}${path ? ` ${path}` : ""}`,
1029
+ },
1030
+ uiTheme,
1031
+ );
1032
+
1027
1033
  const contentText = result.content[0]?.text ?? "";
1028
- // Extract just the content part (after the --- separator)
1029
1034
  const contentBody = contentText.includes("---\n\n")
1030
1035
  ? contentText.split("---\n\n").slice(1).join("---\n\n")
1031
1036
  : contentText;
1032
1037
  const lineCount = countNonEmptyLines(contentBody);
1033
1038
  const charCount = contentBody.trim().length;
1034
- const contentLines = contentBody.split("\n").filter((l) => l.trim());
1035
-
1036
- if (!expanded) {
1037
- // Collapsed view: metadata + preview
1038
- const metaLines: string[] = [
1039
- `${uiTheme.fg("muted", "Content-Type:")} ${details.contentType || "unknown"}`,
1040
- `${uiTheme.fg("muted", "Method:")} ${details.method}`,
1041
- ];
1042
- if (hasRedirect) {
1043
- metaLines.push(`${uiTheme.fg("muted", "Final URL:")} ${uiTheme.fg("mdLinkUrl", details.finalUrl)}`);
1044
- }
1045
- if (truncated) {
1046
- metaLines.push(uiTheme.fg("warning", `${uiTheme.status.warning} Output truncated`));
1047
- if (truncation?.artifactId) {
1048
- metaLines.push(uiTheme.fg("warning", `Full output: artifact://${truncation.artifactId}`));
1049
- }
1050
- }
1051
- if (hasNotes) {
1052
- metaLines.push(`${uiTheme.fg("muted", "Notes:")} ${details.notes.join("; ")}`);
1053
- }
1054
-
1055
- const previewLimit = 3;
1056
- const previewList = applyListLimit(contentLines, { headLimit: previewLimit });
1057
- const previewLines = previewList.items.map((line) => truncate(line.trim(), 100, uiTheme.format.ellipsis));
1058
- const detailLines: string[] = [...metaLines];
1059
-
1060
- if (previewLines.length === 0) {
1061
- detailLines.push(uiTheme.fg("dim", "(no content)"));
1062
- } else {
1063
- for (const line of previewLines) {
1064
- detailLines.push(uiTheme.fg("dim", line));
1065
- }
1066
- }
1067
-
1068
- const remaining = Math.max(0, contentLines.length - previewLines.length);
1069
- if (remaining > 0) {
1070
- detailLines.push(uiTheme.fg("muted", `${uiTheme.format.ellipsis} ${remaining} more lines`));
1071
- } else {
1072
- const lineLabel = `${lineCount} line${lineCount === 1 ? "" : "s"}`;
1073
- detailLines.push(uiTheme.fg("muted", `${lineLabel}${uiTheme.sep.dot}${charCount} chars`));
1074
- }
1075
-
1076
- for (let i = 0; i < detailLines.length; i++) {
1077
- const isLast = i === detailLines.length - 1;
1078
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.vertical;
1079
- text += `\n ${uiTheme.fg("dim", branch)} ${detailLines[i]}`;
1080
- }
1081
- } else {
1082
- // Expanded view: structured metadata + bounded content preview
1083
- const metaLines: string[] = [
1084
- `${uiTheme.fg("muted", "Content-Type:")} ${details.contentType || "unknown"}`,
1085
- `${uiTheme.fg("muted", "Method:")} ${details.method}`,
1086
- ];
1087
- if (hasRedirect) {
1088
- metaLines.push(`${uiTheme.fg("muted", "Final URL:")} ${uiTheme.fg("mdLinkUrl", details.finalUrl)}`);
1089
- }
1090
- const lineLabel = `${lineCount} line${lineCount === 1 ? "" : "s"}`;
1091
- metaLines.push(`${uiTheme.fg("muted", "Lines:")} ${lineLabel}`);
1092
- metaLines.push(`${uiTheme.fg("muted", "Chars:")} ${charCount}`);
1093
- if (truncated) {
1094
- metaLines.push(uiTheme.fg("warning", `${uiTheme.status.warning} Output truncated`));
1095
- if (truncation?.artifactId) {
1096
- metaLines.push(uiTheme.fg("warning", `Full output: artifact://${truncation.artifactId}`));
1097
- }
1098
- }
1099
- if (hasNotes) {
1100
- metaLines.push(`${uiTheme.fg("muted", "Notes:")} ${details.notes.join("; ")}`);
1101
- }
1039
+ const contentLines = contentBody.split("\n").filter(l => l.trim());
1102
1040
 
1103
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.branch)} ${uiTheme.fg("accent", "Metadata")}`;
1104
- for (let i = 0; i < metaLines.length; i++) {
1105
- const isLast = i === metaLines.length - 1;
1106
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
1107
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.vertical)} ${uiTheme.fg("dim", branch)} ${metaLines[i]}`;
1108
- }
1109
-
1110
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("accent", "Content Preview")}`;
1111
- const previewLimit = 12;
1112
- const previewList = applyListLimit(contentLines, { headLimit: previewLimit });
1113
- const previewLines = previewList.items.map((line) => truncate(line.trim(), 120, uiTheme.format.ellipsis));
1114
- const remaining = Math.max(0, contentLines.length - previewLines.length);
1115
- const contentPrefix = uiTheme.fg("dim", " ");
1116
-
1117
- if (previewLines.length === 0) {
1118
- text += `\n ${contentPrefix} ${uiTheme.fg("dim", "(no content)")}`;
1119
- } else {
1120
- for (const line of previewLines) {
1121
- text += `\n ${contentPrefix} ${uiTheme.fg("dim", line)}`;
1122
- }
1041
+ const metadataLines: string[] = [
1042
+ `${uiTheme.fg("muted", "Content-Type:")} ${details.contentType || "unknown"}`,
1043
+ `${uiTheme.fg("muted", "Method:")} ${details.method}`,
1044
+ ];
1045
+ if (hasRedirect) {
1046
+ metadataLines.push(`${uiTheme.fg("muted", "Final URL:")} ${uiTheme.fg("mdLinkUrl", details.finalUrl)}`);
1047
+ }
1048
+ const lineLabel = `${lineCount} line${lineCount === 1 ? "" : "s"}`;
1049
+ metadataLines.push(`${uiTheme.fg("muted", "Lines:")} ${lineLabel}`);
1050
+ metadataLines.push(`${uiTheme.fg("muted", "Chars:")} ${charCount}`);
1051
+ if (truncated) {
1052
+ metadataLines.push(uiTheme.fg("warning", `${uiTheme.status.warning} Output truncated`));
1053
+ if (truncation?.artifactId) {
1054
+ metadataLines.push(uiTheme.fg("warning", `Full output: artifact://${truncation.artifactId}`));
1123
1055
  }
1056
+ }
1057
+ if (hasNotes) {
1058
+ metadataLines.push(`${uiTheme.fg("muted", "Notes:")} ${details.notes.join("; ")}`);
1059
+ }
1124
1060
 
1125
- if (remaining > 0) {
1126
- text += `\n ${contentPrefix} ${uiTheme.fg("muted", `${uiTheme.format.ellipsis} ${remaining} more lines`)}`;
1127
- }
1061
+ const previewLimit = expanded ? 12 : 3;
1062
+ const previewList = applyListLimit(contentLines, { headLimit: previewLimit });
1063
+ const previewLines = previewList.items.map(line => truncate(line.trimEnd(), 120, uiTheme.format.ellipsis));
1064
+ const remaining = Math.max(0, contentLines.length - previewLines.length);
1065
+ const contentPreviewLines =
1066
+ previewLines.length > 0 ? previewLines.map(line => uiTheme.fg("dim", line)) : [uiTheme.fg("dim", "(no content)")];
1067
+ if (remaining > 0) {
1068
+ const hint = formatExpandHint(uiTheme, expanded, true);
1069
+ contentPreviewLines.push(
1070
+ uiTheme.fg("muted", `${uiTheme.format.ellipsis} ${remaining} more lines${hint ? ` ${hint}` : ""}`),
1071
+ );
1128
1072
  }
1129
1073
 
1130
- return new Text(text, 0, 0);
1074
+ return {
1075
+ render: (width: number) =>
1076
+ renderOutputBlock(
1077
+ {
1078
+ header,
1079
+ state: truncated ? "warning" : "success",
1080
+ sections: [
1081
+ { label: uiTheme.fg("toolTitle", "Metadata"), lines: metadataLines },
1082
+ { label: uiTheme.fg("toolTitle", "Content Preview"), lines: contentPreviewLines },
1083
+ ],
1084
+ width,
1085
+ },
1086
+ uiTheme,
1087
+ ),
1088
+ invalidate: () => {},
1089
+ };
1131
1090
  }
1132
1091
 
1133
1092
  export const fetchToolRenderer = {
1134
1093
  renderCall: renderFetchCall,
1135
1094
  renderResult: renderFetchResult,
1095
+ mergeCallAndResult: true,
1136
1096
  };
package/src/tools/find.ts CHANGED
@@ -1,23 +1,23 @@
1
1
  import path from "node:path";
2
2
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
3
3
  import { StringEnum } from "@oh-my-pi/pi-ai";
4
- import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
5
- import type { RenderResultOptions } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
6
- import { getLanguageFromPath, type Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
7
- import findDescription from "@oh-my-pi/pi-coding-agent/prompts/tools/find.md" with { type: "text" };
8
- import type { OutputMeta } from "@oh-my-pi/pi-coding-agent/tools/output-meta";
9
- import { ToolAbortError, ToolError, throwIfAborted } from "@oh-my-pi/pi-coding-agent/tools/tool-errors";
10
- import { ensureTool } from "@oh-my-pi/pi-coding-agent/utils/tools-manager";
11
4
  import type { Component } from "@oh-my-pi/pi-tui";
12
5
  import { Text } from "@oh-my-pi/pi-tui";
13
6
  import { ptree, untilAborted } from "@oh-my-pi/pi-utils";
14
7
  import type { Static } from "@sinclair/typebox";
15
8
  import { Type } from "@sinclair/typebox";
16
-
17
- import type { ToolSession } from "./index";
9
+ import { renderPromptTemplate } from "../config/prompt-templates";
10
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
+ import type { Theme } from "../modes/theme/theme";
12
+ import findDescription from "../prompts/tools/find.md" with { type: "text" };
13
+ import { renderFileList, renderStatusLine, renderTreeList } from "../tui";
14
+ import { ensureTool } from "../utils/tools-manager";
15
+ import type { ToolSession } from ".";
18
16
  import { applyListLimit } from "./list-limit";
17
+ import type { OutputMeta } from "./output-meta";
19
18
  import { resolveToCwd } from "./path-utils";
20
- import { PREVIEW_LIMITS, ToolUIKit } from "./render-utils";
19
+ import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
20
+ import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
21
21
  import { toolResult } from "./tool-result";
22
22
  import { type TruncationResult, truncateHead } from "./truncate";
23
23
 
@@ -34,6 +34,7 @@ const findSchema = Type.Object({
34
34
  });
35
35
 
36
36
  const DEFAULT_LIMIT = 1000;
37
+ const FD_TIMEOUT_MS = 5000;
37
38
 
38
39
  export interface FindToolDetails {
39
40
  truncation?: TruncationResult;
@@ -132,6 +133,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
132
133
 
133
134
  return untilAborted(signal, async () => {
134
135
  const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
136
+
137
+ if (searchPath === "/") {
138
+ throw new ToolError("Searching from root directory '/' is not allowed");
139
+ }
140
+
135
141
  const scopePath = (() => {
136
142
  const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
137
143
  return relative.length === 0 ? "." : relative;
@@ -157,7 +163,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
157
163
  }
158
164
 
159
165
  // Relativize paths
160
- const relativized = results.map((p) => {
166
+ const relativized = results.map(p => {
161
167
  if (p.startsWith(searchPath)) {
162
168
  return p.slice(searchPath.length + 1);
163
169
  }
@@ -246,7 +252,9 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
246
252
  "--absolute-path",
247
253
  searchPath,
248
254
  ];
249
- const { stdout: gitignoreStdout } = await runFd(fdPath, gitignoreArgs, signal);
255
+ const timeoutSignal = AbortSignal.timeout(FD_TIMEOUT_MS);
256
+ const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
257
+ const { stdout: gitignoreStdout } = await runFd(fdPath, gitignoreArgs, combinedSignal);
250
258
  for (const rawLine of gitignoreStdout.split("\n")) {
251
259
  const file = rawLine.trim();
252
260
  if (!file) continue;
@@ -266,8 +274,10 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
266
274
  // Pattern and path
267
275
  args.push(effectivePattern, searchPath);
268
276
 
269
- // Run fd
270
- const { stdout, stderr, exitCode } = await runFd(fdPath, args, signal);
277
+ // Run fd with timeout
278
+ const mainTimeoutSignal = AbortSignal.timeout(FD_TIMEOUT_MS);
279
+ const mainCombinedSignal = signal ? AbortSignal.any([signal, mainTimeoutSignal]) : mainTimeoutSignal;
280
+ const { stdout, stderr, exitCode } = await runFd(fdPath, args, mainCombinedSignal);
271
281
  const output = stdout.trim();
272
282
 
273
283
  // fd exit codes: 0 = found files, 1 = no matches, other = error
@@ -320,7 +330,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
320
330
  const indexed = relativized.map((path, idx) => ({ path, mtime: mtimes[idx] }));
321
331
  indexed.sort((a, b) => b.mtime - a.mtime);
322
332
  relativized.length = 0;
323
- relativized.push(...indexed.map((item) => item.path));
333
+ relativized.push(...indexed.map(item => item.path));
324
334
  }
325
335
 
326
336
  const listLimit = applyListLimit(relativized, { limit: effectiveLimit });
@@ -371,10 +381,6 @@ const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
371
381
  export const findToolRenderer = {
372
382
  inline: true,
373
383
  renderCall(args: FindRenderArgs, uiTheme: Theme): Component {
374
- const ui = new ToolUIKit(uiTheme);
375
- const label = ui.title("Find");
376
- let text = `${uiTheme.format.bullet} ${label} ${uiTheme.fg("accent", args.pattern || "*")}`;
377
-
378
384
  const meta: string[] = [];
379
385
  if (args.path) meta.push(`in ${args.path}`);
380
386
  if (args.type && args.type !== "all") meta.push(`type:${args.type}`);
@@ -382,8 +388,10 @@ export const findToolRenderer = {
382
388
  if (args.sortByMtime) meta.push("sort:mtime");
383
389
  if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
384
390
 
385
- text += ui.meta(meta);
386
-
391
+ const text = renderStatusLine(
392
+ { icon: "pending", title: "Find", description: args.pattern || "*", meta },
393
+ uiTheme,
394
+ );
387
395
  return new Text(text, 0, 0);
388
396
  },
389
397
 
@@ -391,106 +399,94 @@ export const findToolRenderer = {
391
399
  result: { content: Array<{ type: string; text?: string }>; details?: FindToolDetails; isError?: boolean },
392
400
  { expanded }: RenderResultOptions,
393
401
  uiTheme: Theme,
402
+ args?: FindRenderArgs,
394
403
  ): Component {
395
- const ui = new ToolUIKit(uiTheme);
396
404
  const details = result.details;
397
405
 
398
406
  if (result.isError || details?.error) {
399
- const errorText = details?.error || result.content?.find((c) => c.type === "text")?.text || "Unknown error";
400
- return new Text(` ${ui.errorMessage(errorText)}`, 0, 0);
407
+ const errorText = details?.error || result.content?.find(c => c.type === "text")?.text || "Unknown error";
408
+ return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
401
409
  }
402
410
 
403
411
  const hasDetailedData = details?.fileCount !== undefined;
404
- const textContent = result.content?.find((c) => c.type === "text")?.text;
412
+ const textContent = result.content?.find(c => c.type === "text")?.text;
405
413
 
406
414
  if (!hasDetailedData) {
407
- if (!textContent || textContent.includes("No files matching") || textContent.trim() === "") {
408
- return new Text(` ${ui.emptyMessage("No files found")}`, 0, 0);
415
+ if (
416
+ !textContent ||
417
+ textContent.includes("No files matching") ||
418
+ textContent.includes("No files found") ||
419
+ textContent.trim() === ""
420
+ ) {
421
+ return new Text(formatEmptyMessage("No files found", uiTheme), 0, 0);
409
422
  }
410
423
 
411
- const lines = textContent.split("\n").filter((l) => l.trim());
412
- const maxLines = expanded ? lines.length : Math.min(lines.length, COLLAPSED_LIST_LIMIT);
413
- const displayLines = lines.slice(0, maxLines);
414
- const remaining = lines.length - maxLines;
415
- const hasMore = remaining > 0;
416
-
417
- const icon = uiTheme.styledSymbol("status.success", "success");
418
- const summary = ui.count("file", lines.length);
419
- const expandHint = ui.expandHint(expanded, hasMore);
420
- let text = ` ${icon} ${uiTheme.fg("dim", summary)}${expandHint}`;
421
-
422
- for (let i = 0; i < displayLines.length; i++) {
423
- const isLast = i === displayLines.length - 1 && remaining === 0;
424
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
425
- text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", displayLines[i])}`;
426
- }
427
- if (remaining > 0) {
428
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", ui.moreItems(remaining, "file"))}`;
429
- }
430
- return new Text(text, 0, 0);
424
+ const lines = textContent.split("\n").filter(l => l.trim());
425
+ const header = renderStatusLine(
426
+ {
427
+ icon: "success",
428
+ title: "Find",
429
+ description: args?.pattern,
430
+ meta: [formatCount("file", lines.length)],
431
+ },
432
+ uiTheme,
433
+ );
434
+ const listLines = renderTreeList(
435
+ {
436
+ items: lines,
437
+ expanded,
438
+ maxCollapsed: COLLAPSED_LIST_LIMIT,
439
+ itemType: "file",
440
+ renderItem: line => uiTheme.fg("accent", line),
441
+ },
442
+ uiTheme,
443
+ );
444
+ return new Text([header, ...listLines].join("\n"), 0, 0);
431
445
  }
432
446
 
433
447
  const fileCount = details?.fileCount ?? 0;
434
- const truncation = details?.meta?.truncation;
448
+ const truncation = details?.truncation ?? details?.meta?.truncation;
435
449
  const limits = details?.meta?.limits;
436
- const truncated = Boolean(
437
- details?.truncated || truncation || limits?.resultLimit || limits?.headLimit || limits?.matchLimit,
438
- );
450
+ const truncated = Boolean(details?.truncated || truncation || details?.resultLimitReached || limits?.resultLimit);
439
451
  const files = details?.files ?? [];
440
452
 
441
453
  if (fileCount === 0) {
442
- return new Text(` ${ui.emptyMessage("No files found")}`, 0, 0);
454
+ const header = renderStatusLine(
455
+ { icon: "warning", title: "Find", description: args?.pattern, meta: ["0 files"] },
456
+ uiTheme,
457
+ );
458
+ return new Text([header, formatEmptyMessage("No files found", uiTheme)].join("\n"), 0, 0);
443
459
  }
460
+ const meta: string[] = [formatCount("file", fileCount)];
461
+ if (details?.scopePath) meta.push(`in ${details.scopePath}`);
462
+ if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
463
+ const header = renderStatusLine(
464
+ { icon: truncated ? "warning" : "success", title: "Find", description: args?.pattern, meta },
465
+ uiTheme,
466
+ );
444
467
 
445
- const icon = uiTheme.styledSymbol("status.success", "success");
446
- const summaryText = ui.count("file", fileCount);
447
- const scopeLabel = ui.scope(details?.scopePath);
448
- const maxFiles = expanded ? files.length : Math.min(files.length, COLLAPSED_LIST_LIMIT);
449
- const hasMoreFiles = files.length > maxFiles;
450
- const expandHint = ui.expandHint(expanded, hasMoreFiles);
451
-
452
- let text = ` ${icon} ${uiTheme.fg("dim", summaryText)}${ui.truncationSuffix(truncated)}${scopeLabel}${expandHint}`;
468
+ const fileLines = renderFileList(
469
+ {
470
+ files: files.map(entry => ({ path: entry, isDirectory: entry.endsWith("/") })),
471
+ expanded,
472
+ maxCollapsed: COLLAPSED_LIST_LIMIT,
473
+ },
474
+ uiTheme,
475
+ );
453
476
 
454
477
  const truncationReasons: string[] = [];
455
- if (limits?.resultLimit) {
456
- truncationReasons.push(`limit ${limits.resultLimit.reached} results`);
457
- }
458
- if (truncation) {
459
- truncationReasons.push(truncation.truncatedBy === "lines" ? "line limit" : "size limit");
460
- }
461
- if (truncation?.artifactId) {
462
- truncationReasons.push(`full output: artifact://${truncation.artifactId}`);
478
+ if (details?.resultLimitReached) truncationReasons.push(`limit ${details.resultLimitReached} results`);
479
+ if (limits?.resultLimit) truncationReasons.push(`limit ${limits.resultLimit.reached} results`);
480
+ if (truncation) truncationReasons.push(truncation.truncatedBy === "lines" ? "line limit" : "size limit");
481
+ const artifactId = truncation && "artifactId" in truncation ? truncation.artifactId : undefined;
482
+ if (artifactId) truncationReasons.push(`full output: artifact://${artifactId}`);
483
+
484
+ const extraLines: string[] = [];
485
+ if (truncationReasons.length > 0) {
486
+ extraLines.push(uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`));
463
487
  }
464
488
 
465
- const hasTruncation = truncationReasons.length > 0;
466
-
467
- if (files.length > 0) {
468
- for (let i = 0; i < maxFiles; i++) {
469
- const isLast = i === maxFiles - 1 && !hasMoreFiles && !hasTruncation;
470
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
471
- const entry = files[i];
472
- const isDir = entry.endsWith("/");
473
- const entryPath = isDir ? entry.slice(0, -1) : entry;
474
- const lang = isDir ? undefined : getLanguageFromPath(entryPath);
475
- const entryIcon = isDir
476
- ? uiTheme.fg("accent", uiTheme.icon.folder)
477
- : uiTheme.fg("muted", uiTheme.getLangIcon(lang));
478
- text += `\n ${uiTheme.fg("dim", branch)} ${entryIcon} ${uiTheme.fg("accent", entry)}`;
479
- }
480
-
481
- if (hasMoreFiles) {
482
- const moreFilesBranch = hasTruncation ? uiTheme.tree.branch : uiTheme.tree.last;
483
- text += `\n ${uiTheme.fg("dim", moreFilesBranch)} ${uiTheme.fg(
484
- "muted",
485
- ui.moreItems(files.length - maxFiles, "file"),
486
- )}`;
487
- }
488
- }
489
-
490
- if (hasTruncation) {
491
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)}`;
492
- }
493
-
494
- return new Text(text, 0, 0);
489
+ return new Text([header, ...fileLines, ...extraLines].join("\n"), 0, 0);
495
490
  },
491
+ mergeCallAndResult: true,
496
492
  };
@@ -1,15 +1,15 @@
1
- import { tmpdir } from "node:os";
2
- import { join } from "node:path";
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
3
  import { StringEnum } from "@oh-my-pi/pi-ai";
4
- import type { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
5
- import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
6
- import type { CustomTool } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
7
- import geminiImageDescription from "@oh-my-pi/pi-coding-agent/prompts/tools/gemini-image.md" with { type: "text" };
8
- import { detectSupportedImageMimeTypeFromFile } from "@oh-my-pi/pi-coding-agent/utils/mime";
9
- import { getEnv } from "@oh-my-pi/pi-coding-agent/web/search/auth";
10
4
  import { untilAborted } from "@oh-my-pi/pi-utils";
11
5
  import { type Static, Type } from "@sinclair/typebox";
12
6
  import { nanoid } from "nanoid";
7
+ import type { ModelRegistry } from "../config/model-registry";
8
+ import { renderPromptTemplate } from "../config/prompt-templates";
9
+ import type { CustomTool } from "../extensibility/custom-tools/types";
10
+ import geminiImageDescription from "../prompts/tools/gemini-image.md" with { type: "text" };
11
+ import { detectSupportedImageMimeTypeFromFile } from "../utils/mime";
12
+ import { getEnv } from "../web/search/auth";
13
13
  import { resolveReadPath } from "./path-utils";
14
14
 
15
15
  const DEFAULT_MODEL = "gemini-3-pro-image-preview";
@@ -151,7 +151,7 @@ function assemblePrompt(params: GeminiImageParams): string {
151
151
  if (params.style) parts.push(params.style);
152
152
 
153
153
  // Join with periods for sentence structure
154
- let prompt = `${parts.map((p) => p.replace(/[.!,;:]+$/, "")).join(". ")}.`;
154
+ let prompt = `${parts.map(p => p.replace(/[.!,;:]+$/, "")).join(". ")}.`;
155
155
 
156
156
  // Text rendering specs
157
157
  if (params.text) {
@@ -160,7 +160,7 @@ function assemblePrompt(params: GeminiImageParams): string {
160
160
 
161
161
  // Edit mode: changes and preserve directives
162
162
  if (params.changes?.length) {
163
- prompt += `\n\nChanges:\n${params.changes.map((c) => `- ${c}`).join("\n")}`;
163
+ prompt += `\n\nChanges:\n${params.changes.map(c => `- ${c}`).join("\n")}`;
164
164
  if (params.preserve) {
165
165
  prompt += `\n\nPreserve: ${params.preserve}`;
166
166
  }
@@ -330,8 +330,8 @@ function collectOpenRouterResponseText(message: OpenRouterMessage | undefined):
330
330
  }
331
331
  if (Array.isArray(message.content)) {
332
332
  const texts = message.content
333
- .filter((part) => part.type === "text")
334
- .map((part) => part.text)
333
+ .filter(part => part.type === "text")
334
+ .map(part => part.text)
335
335
  .filter((text): text is string => Boolean(text));
336
336
  const combined = texts.join("\n").trim();
337
337
  return combined.length > 0 ? combined : undefined;
@@ -489,7 +489,7 @@ function getExtensionForMime(mimeType: string): string {
489
489
  async function saveImageToTemp(image: InlineImageData): Promise<string> {
490
490
  const ext = getExtensionForMime(image.mimeType);
491
491
  const filename = `omp-image-${nanoid()}.${ext}`;
492
- const filepath = join(tmpdir(), filename);
492
+ const filepath = path.join(os.tmpdir(), filename);
493
493
  await Bun.write(filepath, Buffer.from(image.data, "base64"));
494
494
  return filepath;
495
495
  }
@@ -515,7 +515,7 @@ function buildResponseSummary(
515
515
  }
516
516
 
517
517
  function collectResponseText(parts: GeminiPart[]): string | undefined {
518
- const texts = parts.map((part) => part.text).filter((text): text is string => Boolean(text));
518
+ const texts = parts.map(part => part.text).filter((text): text is string => Boolean(text));
519
519
  const combined = texts.join("\n").trim();
520
520
  return combined.length > 0 ? combined : undefined;
521
521
  }