@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
@@ -2,16 +2,18 @@ import { randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
4
  import { StringEnum } from "@oh-my-pi/pi-ai";
5
- import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
6
- import type { RenderResultOptions } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
7
- import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
8
- import todoWriteDescription from "@oh-my-pi/pi-coding-agent/prompts/tools/todo-write.md" with { type: "text" };
9
- import type { ToolSession } from "@oh-my-pi/pi-coding-agent/sdk";
10
5
  import type { Component } from "@oh-my-pi/pi-tui";
11
6
  import { Text } from "@oh-my-pi/pi-tui";
12
7
  import { logger } from "@oh-my-pi/pi-utils";
13
8
  import { Type } from "@sinclair/typebox";
14
9
  import chalk from "chalk";
10
+ import { renderPromptTemplate } from "../config/prompt-templates";
11
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
12
+ import type { Theme } from "../modes/theme/theme";
13
+ import todoWriteDescription from "../prompts/tools/todo-write.md" with { type: "text" };
14
+ import type { ToolSession } from "../sdk";
15
+ import { renderStatusLine, renderTreeList } from "../tui";
16
+ import { PREVIEW_LIMITS } from "./render-utils";
15
17
 
16
18
  const todoWriteSchema = Type.Object({
17
19
  todos: Type.Array(
@@ -61,7 +63,7 @@ function normalizeTodoStatus(status?: string): TodoStatus {
61
63
  }
62
64
 
63
65
  function normalizeTodos(items: Array<{ id?: string; content?: string; status?: string }>): TodoItem[] {
64
- return items.map((item) => {
66
+ return items.map(item => {
65
67
  if (!item.content) {
66
68
  throw new Error("Todo content is required.");
67
69
  }
@@ -80,7 +82,7 @@ function normalizeTodos(items: Array<{ id?: string; content?: string; status?: s
80
82
  function validateSequentialTodos(todos: TodoItem[]): { valid: boolean; error?: string } {
81
83
  if (todos.length === 0) return { valid: true };
82
84
 
83
- const firstIncompleteIndex = todos.findIndex((todo) => todo.status !== "completed");
85
+ const firstIncompleteIndex = todos.findIndex(todo => todo.status !== "completed");
84
86
  if (firstIncompleteIndex >= 0) {
85
87
  for (let i = firstIncompleteIndex + 1; i < todos.length; i++) {
86
88
  if (todos[i].status === "completed") {
@@ -126,9 +128,9 @@ async function loadTodoFile(filePath: string): Promise<TodoFile | null> {
126
128
 
127
129
  function formatTodoSummary(todos: TodoItem[]): string {
128
130
  if (todos.length === 0) return "Todo list cleared.";
129
- const completed = todos.filter((t) => t.status === "completed").length;
130
- const inProgress = todos.filter((t) => t.status === "in_progress").length;
131
- const pending = todos.filter((t) => t.status === "pending").length;
131
+ const completed = todos.filter(t => t.status === "completed").length;
132
+ const inProgress = todos.filter(t => t.status === "in_progress").length;
133
+ const pending = todos.filter(t => t.status === "pending").length;
132
134
  return `Saved ${todos.length} todos (${pending} pending, ${inProgress} in progress, ${completed} completed).`;
133
135
  }
134
136
 
@@ -217,29 +219,39 @@ interface TodoWriteRenderArgs {
217
219
  export const todoWriteToolRenderer = {
218
220
  renderCall(args: TodoWriteRenderArgs, uiTheme: Theme): Component {
219
221
  const count = args.todos?.length ?? 0;
220
- const summary = count > 0 ? uiTheme.fg("accent", `${count} items`) : uiTheme.fg("toolOutput", "empty");
221
- return new Text(`${uiTheme.fg("toolTitle", uiTheme.bold("Todo Write"))} ${summary}`, 0, 0);
222
+ const meta = count > 0 ? [`${count} items`] : ["empty"];
223
+ const text = renderStatusLine({ icon: "pending", title: "Todo Write", meta }, uiTheme);
224
+ return new Text(text, 0, 0);
222
225
  },
223
226
 
224
227
  renderResult(
225
228
  result: { content: Array<{ type: string; text?: string }>; details?: TodoWriteToolDetails },
226
- _options: RenderResultOptions,
229
+ options: RenderResultOptions,
227
230
  uiTheme: Theme,
228
231
  _args?: TodoWriteRenderArgs,
229
232
  ): Component {
233
+ const { expanded } = options;
230
234
  const todos = result.details?.todos ?? [];
231
- const indent = " ";
232
- const hook = uiTheme.tree.hook;
233
- const lines = [indent + uiTheme.bold(uiTheme.fg("accent", "Todos"))];
234
-
235
- if (todos.length > 0) {
236
- const visibleTodos = todos;
237
- visibleTodos.forEach((todo, index) => {
238
- const prefix = `${indent}${index === 0 ? hook : " "} `;
239
- lines.push(formatTodoLine(todo, uiTheme, prefix));
240
- });
235
+ const header = renderStatusLine(
236
+ { icon: "success", title: "Todo Write", meta: [`${todos.length} items`] },
237
+ uiTheme,
238
+ );
239
+ if (todos.length === 0) {
240
+ const fallback = result.content?.find(c => c.type === "text")?.text ?? "No todos";
241
+ return new Text([header, uiTheme.fg("dim", fallback)].join("\n"), 0, 0);
241
242
  }
242
-
243
- return new Text(lines.join("\n"), 0, 0);
243
+ const lines = renderTreeList(
244
+ {
245
+ items: todos,
246
+ expanded,
247
+ maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
248
+ itemType: "todo",
249
+ renderItem: todo => formatTodoLine(todo, uiTheme, ""),
250
+ },
251
+ uiTheme,
252
+ );
253
+
254
+ return new Text([header, ...lines].join("\n"), 0, 0);
244
255
  },
256
+ mergeCallAndResult: true,
245
257
  };
@@ -39,7 +39,7 @@ export class MultiError extends ToolError {
39
39
  readonly errors: ErrorEntry[];
40
40
 
41
41
  constructor(errors: ErrorEntry[]) {
42
- super(errors.map((e) => e.message).join("; "));
42
+ super(errors.map(e => e.message).join("; "));
43
43
  this.name = "MultiError";
44
44
  this.errors = errors;
45
45
  }
@@ -49,11 +49,11 @@ export class MultiError extends ToolError {
49
49
  const e = this.errors[0];
50
50
  return e.context ? `${e.context}: ${e.message}` : e.message;
51
51
  }
52
- return this.errors.map((e) => (e.context ? `${e.context}: ${e.message}` : e.message)).join("\n");
52
+ return this.errors.map(e => (e.context ? `${e.context}: ${e.message}` : e.message)).join("\n");
53
53
  }
54
54
 
55
55
  static from(errors: Array<string | ErrorEntry>): MultiError {
56
- return new MultiError(errors.map((e) => (typeof e === "string" ? { message: e } : e)));
56
+ return new MultiError(errors.map(e => (typeof e === "string" ? { message: e } : e)));
57
57
  }
58
58
  }
59
59
 
@@ -1,13 +1,8 @@
1
1
  import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
2
  import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
3
- import type { OutputSummary } from "@oh-my-pi/pi-coding-agent/session/streaming-output";
4
- import type {
5
- OutputMeta,
6
- TruncationOptions,
7
- TruncationSummaryOptions,
8
- TruncationTextOptions,
9
- } from "@oh-my-pi/pi-coding-agent/tools/output-meta";
10
- import { outputMeta } from "@oh-my-pi/pi-coding-agent/tools/output-meta";
3
+ import type { OutputSummary } from "../session/streaming-output";
4
+ import type { OutputMeta, TruncationOptions, TruncationSummaryOptions, TruncationTextOptions } from "./output-meta";
5
+ import { outputMeta } from "./output-meta";
11
6
  import type { TruncationResult } from "./truncate";
12
7
 
13
8
  type ToolContent = Array<TextContent | ImageContent>;
@@ -5,24 +5,20 @@ import type {
5
5
  AgentToolUpdateCallback,
6
6
  ToolCallContext,
7
7
  } from "@oh-my-pi/pi-agent-core";
8
- import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
9
- import type { RenderResultOptions } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
10
- import {
11
- createLspWritethrough,
12
- type FileDiagnosticsResult,
13
- type WritethroughCallback,
14
- writethroughNoop,
15
- } from "@oh-my-pi/pi-coding-agent/lsp/index";
16
- import { getLanguageFromPath, highlightCode, type Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
17
- import writeDescription from "@oh-my-pi/pi-coding-agent/prompts/tools/write.md" with { type: "text" };
18
- import type { ToolSession } from "@oh-my-pi/pi-coding-agent/sdk";
19
- import { type OutputMeta, outputMeta } from "@oh-my-pi/pi-coding-agent/tools/output-meta";
20
8
  import type { Component } from "@oh-my-pi/pi-tui";
21
9
  import { Text } from "@oh-my-pi/pi-tui";
22
10
  import { untilAborted } from "@oh-my-pi/pi-utils";
23
11
  import { Type } from "@sinclair/typebox";
12
+ import { renderPromptTemplate } from "../config/prompt-templates";
13
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
14
+ import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
15
+ import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
16
+ import writeDescription from "../prompts/tools/write.md" with { type: "text" };
17
+ import type { ToolSession } from "../sdk";
18
+ import { renderCodeCell, renderStatusLine } from "../tui";
19
+ import { type OutputMeta, outputMeta } from "./output-meta";
24
20
  import { resolveToCwd } from "./path-utils";
25
- import { formatDiagnostics, formatExpandHint, formatStatusIcon, replaceTabs, shortenPath } from "./render-utils";
21
+ import { formatDiagnostics, shortenPath } from "./render-utils";
26
22
  import type { RenderCallOptions } from "./renderers";
27
23
 
28
24
  const writeSchema = Type.Object({
@@ -48,7 +44,7 @@ function getLspBatchRequest(toolCall: ToolCallContext | undefined): { id: string
48
44
  if (!hasOtherWrites) {
49
45
  return undefined;
50
46
  }
51
- const hasLaterWrites = toolCall.toolCalls.slice(toolCall.index + 1).some((call) => LSP_BATCH_TOOLS.has(call.name));
47
+ const hasLaterWrites = toolCall.toolCalls.slice(toolCall.index + 1).some(call => LSP_BATCH_TOOLS.has(call.name));
52
48
  return { id: toolCall.batchId, flush: !hasLaterWrites };
53
49
  }
54
50
 
@@ -134,27 +130,6 @@ function countLines(text: string): number {
134
130
  return text.split("\n").length;
135
131
  }
136
132
 
137
- function formatStreamingContent(content: string, rawPath: string, uiTheme: Theme): string {
138
- if (!content) return "";
139
- const lang = getLanguageFromPath(rawPath);
140
- const lines = content.split("\n");
141
- const total = lines.length;
142
- const displayLines = lines.slice(-WRITE_STREAMING_PREVIEW_LINES);
143
- const hidden = total - displayLines.length;
144
-
145
- const formattedLines = lang
146
- ? highlightCode(replaceTabs(displayLines.join("\n")), lang)
147
- : displayLines.map((line: string) => uiTheme.fg("toolOutput", replaceTabs(line)));
148
-
149
- let text = "\n\n";
150
- if (hidden > 0) {
151
- text += uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${hidden} earlier lines)\n`);
152
- }
153
- text += formattedLines.join("\n");
154
- text += uiTheme.fg("dim", `\n${uiTheme.format.ellipsis} (streaming)`);
155
- return text;
156
- }
157
-
158
133
  function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
159
134
  const icon = uiTheme.getLangIcon(language);
160
135
  if (lineCount !== null) {
@@ -167,68 +142,117 @@ export const writeToolRenderer = {
167
142
  renderCall(args: WriteRenderArgs, uiTheme: Theme, options?: RenderCallOptions): Component {
168
143
  const rawPath = args.file_path || args.path || "";
169
144
  const filePath = shortenPath(rawPath);
170
- const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
171
- const spinner =
172
- options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
173
- let text = `${uiTheme.fg("toolTitle", uiTheme.bold("Write"))} ${spinner ? `${spinner} ` : ""}${pathDisplay}`;
174
-
175
- // Show streaming preview of content
176
- if (args.content) {
177
- text += formatStreamingContent(args.content, rawPath, uiTheme);
145
+ const pathDisplay = filePath || uiTheme.format.ellipsis;
146
+ const status = options?.spinnerFrame !== undefined ? "running" : "pending";
147
+ const text = renderStatusLine(
148
+ { icon: status, title: "Write", description: pathDisplay, spinnerFrame: options?.spinnerFrame },
149
+ uiTheme,
150
+ );
151
+ if (!args.content) {
152
+ return new Text(text, 0, 0);
178
153
  }
179
154
 
180
- return new Text(text, 0, 0);
155
+ const contentLines = args.content.split("\n");
156
+ const displayLines = contentLines.slice(-WRITE_STREAMING_PREVIEW_LINES);
157
+ const hidden = contentLines.length - displayLines.length;
158
+ const outputLines: string[] = [];
159
+ if (hidden > 0) {
160
+ outputLines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${hidden} earlier lines)`));
161
+ }
162
+ outputLines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (streaming)`));
163
+
164
+ return {
165
+ render: (width: number) =>
166
+ renderCodeCell(
167
+ {
168
+ code: displayLines.join("\n"),
169
+ language: getLanguageFromPath(rawPath),
170
+ title: filePath ? `Write ${filePath}` : "Write",
171
+ status,
172
+ spinnerFrame: options?.spinnerFrame,
173
+ output: outputLines.join("\n"),
174
+ codeMaxLines: WRITE_STREAMING_PREVIEW_LINES,
175
+ expanded: true,
176
+ width,
177
+ },
178
+ uiTheme,
179
+ ),
180
+ invalidate: () => {},
181
+ };
181
182
  },
182
183
 
183
184
  renderResult(
184
185
  result: { content: Array<{ type: string; text?: string }>; details?: WriteToolDetails },
185
- { expanded }: RenderResultOptions,
186
+ { expanded, isPartial, spinnerFrame }: RenderResultOptions,
186
187
  uiTheme: Theme,
187
188
  args?: WriteRenderArgs,
188
189
  ): Component {
189
190
  const rawPath = args?.file_path || args?.path || "";
191
+ const filePath = shortenPath(rawPath);
190
192
  const fileContent = args?.content || "";
191
193
  const lang = getLanguageFromPath(rawPath);
192
- const contentLines = fileContent
193
- ? lang
194
- ? highlightCode(replaceTabs(fileContent), lang)
195
- : fileContent.split("\n")
196
- : [];
197
- const totalLines = contentLines.length;
198
194
  const outputLines: string[] = [];
195
+ const lineCount = countLines(fileContent);
199
196
 
200
- outputLines.push(formatMetadataLine(countLines(fileContent), lang ?? "text", uiTheme));
197
+ outputLines.push(formatMetadataLine(lineCount, lang ?? "text", uiTheme));
201
198
 
202
- if (fileContent) {
203
- const maxLines = expanded ? contentLines.length : 10;
204
- const displayLines = contentLines.slice(0, maxLines);
205
- const remaining = contentLines.length - maxLines;
199
+ if (isPartial && fileContent) {
200
+ const contentLines = fileContent.split("\n");
201
+ const displayLines = contentLines.slice(-WRITE_STREAMING_PREVIEW_LINES);
202
+ const hidden = contentLines.length - displayLines.length;
203
+ if (hidden > 0) {
204
+ outputLines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${hidden} earlier lines)`));
205
+ }
206
+ outputLines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (streaming)`));
206
207
 
207
- outputLines.push(
208
- "",
209
- ...displayLines.map((line: string) =>
210
- lang ? replaceTabs(line) : uiTheme.fg("toolOutput", replaceTabs(line)),
211
- ),
212
- );
213
- if (remaining > 0) {
214
- outputLines.push(
215
- uiTheme.fg(
216
- "toolOutput",
217
- `${uiTheme.format.ellipsis} (${remaining} more lines, ${totalLines} total) ${formatExpandHint(uiTheme)}`,
208
+ return {
209
+ render: (width: number) =>
210
+ renderCodeCell(
211
+ {
212
+ code: displayLines.join("\n"),
213
+ language: lang,
214
+ title: filePath ? `Write ${filePath}` : "Write",
215
+ status: spinnerFrame !== undefined ? "running" : "pending",
216
+ spinnerFrame,
217
+ output: outputLines.join("\n"),
218
+ codeMaxLines: WRITE_STREAMING_PREVIEW_LINES,
219
+ expanded: true,
220
+ width,
221
+ },
222
+ uiTheme,
218
223
  ),
219
- );
220
- }
224
+ invalidate: () => {},
225
+ };
221
226
  }
222
227
 
223
- // Show LSP diagnostics if available
224
228
  if (result.details?.diagnostics) {
225
- outputLines.push(
226
- formatDiagnostics(result.details.diagnostics, expanded, uiTheme, (fp) =>
227
- uiTheme.getLangIcon(getLanguageFromPath(fp)),
228
- ),
229
+ const diagText = formatDiagnostics(result.details.diagnostics, expanded, uiTheme, fp =>
230
+ uiTheme.getLangIcon(getLanguageFromPath(fp)),
229
231
  );
232
+ if (diagText.trim()) {
233
+ const diagLines = diagText.split("\n");
234
+ const firstNonEmpty = diagLines.findIndex(line => line.trim());
235
+ outputLines.push(...(firstNonEmpty >= 0 ? diagLines.slice(firstNonEmpty) : []));
236
+ }
230
237
  }
231
238
 
232
- return new Text(outputLines.join("\n"), 0, 0);
239
+ return {
240
+ render: (width: number) =>
241
+ renderCodeCell(
242
+ {
243
+ code: fileContent,
244
+ language: lang,
245
+ title: filePath ? `Write ${filePath}` : "Write",
246
+ status: "complete",
247
+ output: outputLines.join("\n"),
248
+ codeMaxLines: expanded ? Number.POSITIVE_INFINITY : 10,
249
+ expanded,
250
+ width,
251
+ },
252
+ uiTheme,
253
+ ),
254
+ invalidate: () => {},
255
+ };
233
256
  },
257
+ mergeCallAndResult: true,
234
258
  };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Render a code cell with optional output section.
3
+ */
4
+ import { highlightCode, type Theme } from "../modes/theme/theme";
5
+ import { formatDuration, formatExpandHint, formatMoreItems, replaceTabs } from "../tools/render-utils";
6
+ import { renderOutputBlock } from "./output-block";
7
+ import type { State } from "./types";
8
+ import { getStateIcon } from "./utils";
9
+
10
+ export interface CodeCellOptions {
11
+ code: string;
12
+ language?: string;
13
+ index?: number;
14
+ total?: number;
15
+ title?: string;
16
+ status?: "pending" | "running" | "complete" | "error";
17
+ spinnerFrame?: number;
18
+ duration?: number;
19
+ output?: string;
20
+ outputMaxLines?: number;
21
+ codeMaxLines?: number;
22
+ expanded?: boolean;
23
+ width: number;
24
+ }
25
+
26
+ function getState(status?: CodeCellOptions["status"]): State | undefined {
27
+ if (!status) return undefined;
28
+ if (status === "complete") return "success";
29
+ if (status === "error") return "error";
30
+ if (status === "running") return "running";
31
+ return "pending";
32
+ }
33
+
34
+ function formatHeader(options: CodeCellOptions, theme: Theme): { title: string; meta?: string } {
35
+ const { index, total, title, status, spinnerFrame, duration } = options;
36
+ const parts: string[] = [];
37
+ if (index !== undefined && total !== undefined) {
38
+ parts.push(theme.fg("accent", `[${index + 1}/${total}]`));
39
+ }
40
+ if (title) {
41
+ parts.push(theme.fg("toolTitle", title));
42
+ }
43
+ const headerTitle = parts.length > 0 ? parts.join(" ") : theme.fg("toolTitle", "Code");
44
+
45
+ const metaParts: string[] = [];
46
+ if (duration !== undefined) {
47
+ metaParts.push(theme.fg("dim", `(${formatDuration(duration)})`));
48
+ }
49
+ if (status) {
50
+ const icon = getStateIcon(
51
+ status === "complete"
52
+ ? "success"
53
+ : status === "error"
54
+ ? "error"
55
+ : status === "running"
56
+ ? "running"
57
+ : "pending",
58
+ theme,
59
+ spinnerFrame,
60
+ );
61
+ if (status === "pending" || status === "running") {
62
+ metaParts.push(`${icon} ${theme.fg("muted", status)}`);
63
+ } else {
64
+ metaParts.push(icon);
65
+ }
66
+ }
67
+
68
+ if (metaParts.length === 0) return { title: headerTitle };
69
+ return { title: headerTitle, meta: metaParts.join(theme.fg("dim", theme.sep.dot)) };
70
+ }
71
+
72
+ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[] {
73
+ const { code, language, output, expanded = false, outputMaxLines = 6, codeMaxLines = 12, width } = options;
74
+ const { title, meta } = formatHeader(options, theme);
75
+ const state = getState(options.status);
76
+
77
+ const rawCodeLines = highlightCode(replaceTabs(code ?? ""), language);
78
+ const maxCodeLines = expanded ? rawCodeLines.length : Math.min(rawCodeLines.length, codeMaxLines);
79
+ const codeLines = rawCodeLines.slice(0, maxCodeLines);
80
+ const hiddenCodeLines = rawCodeLines.length - codeLines.length;
81
+ if (hiddenCodeLines > 0) {
82
+ const hint = formatExpandHint(theme, expanded, hiddenCodeLines > 0);
83
+ const moreLine = `${formatMoreItems(hiddenCodeLines, "line", theme)}${hint ? ` ${hint}` : ""}`;
84
+ codeLines.push(theme.fg("dim", moreLine));
85
+ }
86
+
87
+ const outputLines: string[] = [];
88
+ if (output?.trim()) {
89
+ const rawLines = output.split("\n");
90
+ const maxLines = expanded ? rawLines.length : Math.min(rawLines.length, outputMaxLines);
91
+ const displayLines = rawLines
92
+ .slice(0, maxLines)
93
+ .map(line => (line.includes("\x1b[") ? line : theme.fg("toolOutput", line)));
94
+ outputLines.push(...displayLines);
95
+ const remaining = rawLines.length - maxLines;
96
+ if (remaining > 0) {
97
+ const hint = formatExpandHint(theme, expanded, remaining > 0);
98
+ const moreLine = `${formatMoreItems(remaining, "line", theme)}${hint ? ` ${hint}` : ""}`;
99
+ outputLines.push(theme.fg("dim", moreLine));
100
+ }
101
+ }
102
+
103
+ const sections: Array<{ label?: string; lines: string[] }> = [{ lines: codeLines }];
104
+ if (outputLines.length > 0) {
105
+ sections.push({ label: theme.fg("toolTitle", "Output"), lines: outputLines });
106
+ }
107
+
108
+ return renderOutputBlock({ header: title, headerMeta: meta, state, sections, width }, theme);
109
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Render file listings with optional icons and metadata.
3
+ */
4
+ import type { Theme } from "../modes/theme/theme";
5
+ import { getLanguageFromPath } from "../modes/theme/theme";
6
+ import { renderTreeList } from "./tree-list";
7
+
8
+ export interface FileEntry {
9
+ path: string;
10
+ isDirectory?: boolean;
11
+ meta?: string;
12
+ }
13
+
14
+ export interface FileListOptions {
15
+ files: FileEntry[];
16
+ expanded?: boolean;
17
+ maxCollapsed?: number;
18
+ showIcons?: boolean;
19
+ }
20
+
21
+ export function renderFileList(options: FileListOptions, theme: Theme): string[] {
22
+ const { files, expanded = false, maxCollapsed = 8, showIcons = true } = options;
23
+
24
+ return renderTreeList(
25
+ {
26
+ items: files,
27
+ expanded,
28
+ maxCollapsed,
29
+ itemType: "file",
30
+ renderItem: entry => {
31
+ const isDirectory = entry.isDirectory ?? entry.path.endsWith("/");
32
+ const displayPath = isDirectory && entry.path.endsWith("/") ? entry.path : entry.path;
33
+ const lang = isDirectory ? undefined : getLanguageFromPath(displayPath);
34
+ const icon = !showIcons
35
+ ? ""
36
+ : isDirectory
37
+ ? theme.fg("accent", theme.icon.folder)
38
+ : theme.fg("muted", theme.getLangIcon(lang));
39
+ const labelColor = isDirectory ? "accent" : "toolOutput";
40
+ const meta = entry.meta ? ` ${theme.fg("dim", entry.meta)}` : "";
41
+ const iconPrefix = icon ? `${icon} ` : "";
42
+ return `${iconPrefix}${theme.fg(labelColor, displayPath)}${meta}`;
43
+ },
44
+ },
45
+ theme,
46
+ );
47
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Public exports for shared TUI components.
3
+ */
4
+
5
+ export * from "./code-cell";
6
+ export * from "./file-list";
7
+ export * from "./output-block";
8
+ export * from "./status-line";
9
+ export * from "./tree-list";
10
+ export * from "./types";
11
+ export * from "./utils";
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Bordered output container with optional header and sections.
3
+ */
4
+ import { visibleWidth } from "@oh-my-pi/pi-tui";
5
+ import type { Theme } from "../modes/theme/theme";
6
+ import type { State } from "./types";
7
+ import { getStateBgColor, padToWidth, truncateToWidth } from "./utils";
8
+
9
+ export interface OutputBlockOptions {
10
+ header?: string;
11
+ headerMeta?: string;
12
+ state?: State;
13
+ sections?: Array<{ label?: string; lines: string[] }>;
14
+ width: number;
15
+ }
16
+
17
+ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): string[] {
18
+ const { header, headerMeta, state, sections = [], width } = options;
19
+ const h = theme.boxSharp.horizontal;
20
+ const v = theme.boxSharp.vertical;
21
+ const cap = h.repeat(3);
22
+ const lineWidth = Math.max(0, width);
23
+ // Border colors: running/pending use accent, success uses dim (gray), error/warning keep their colors
24
+ const borderColor: "error" | "warning" | "accent" | "dim" =
25
+ state === "error"
26
+ ? "error"
27
+ : state === "warning"
28
+ ? "warning"
29
+ : state === "running" || state === "pending"
30
+ ? "accent"
31
+ : "dim";
32
+ const border = (text: string) => theme.fg(borderColor, text);
33
+ const bgFn = state ? (text: string) => theme.bg(getStateBgColor(state), text) : undefined;
34
+
35
+ const buildBarLine = (leftChar: string, label?: string, meta?: string): string => {
36
+ const left = border(`${leftChar}${cap}`);
37
+ if (lineWidth <= 0) return left;
38
+ const labelText = [label, meta].filter(Boolean).join(theme.sep.dot);
39
+ const rawLabel = labelText ? ` ${labelText} ` : " ";
40
+ const maxLabelWidth = Math.max(0, lineWidth - visibleWidth(left));
41
+ const trimmedLabel = truncateToWidth(rawLabel, maxLabelWidth, theme.format.ellipsis);
42
+ const fillCount = Math.max(0, lineWidth - visibleWidth(left + trimmedLabel));
43
+ return `${left}${trimmedLabel}${border(h.repeat(fillCount))}`;
44
+ };
45
+
46
+ const contentPrefix = border(`${v} `);
47
+ const contentWidth = Math.max(0, lineWidth - visibleWidth(contentPrefix));
48
+ const lines: string[] = [];
49
+
50
+ lines.push(padToWidth(buildBarLine(theme.boxSharp.topLeft, header, headerMeta), lineWidth, bgFn));
51
+
52
+ const hasSections = sections.length > 0;
53
+ const normalizedSections = hasSections ? sections : [{ lines: [] }];
54
+
55
+ for (let i = 0; i < normalizedSections.length; i++) {
56
+ const section = normalizedSections[i];
57
+ if (section.label) {
58
+ lines.push(padToWidth(buildBarLine(theme.boxSharp.teeRight, section.label), lineWidth, bgFn));
59
+ }
60
+ for (const line of section.lines) {
61
+ const text = truncateToWidth(line, contentWidth, theme.format.ellipsis);
62
+ lines.push(padToWidth(`${contentPrefix}${text}`, lineWidth, bgFn));
63
+ }
64
+ }
65
+
66
+ const bottomLeft = border(`${theme.boxSharp.bottomLeft}${cap}`);
67
+ const bottomFillCount = Math.max(0, lineWidth - visibleWidth(bottomLeft));
68
+ const bottomLine = `${bottomLeft}${border(h.repeat(bottomFillCount))}`;
69
+ lines.push(padToWidth(bottomLine, lineWidth, bgFn));
70
+
71
+ return lines;
72
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Standardized status header rendering for tool output.
3
+ */
4
+ import type { Theme, ThemeColor } from "../modes/theme/theme";
5
+ import type { IconType } from "./types";
6
+ import { getStateIcon } from "./utils";
7
+
8
+ export interface StatusLineOptions {
9
+ icon?: IconType;
10
+ spinnerFrame?: number;
11
+ title: string;
12
+ titleColor?: ThemeColor;
13
+ description?: string;
14
+ badge?: { label: string; color: ThemeColor };
15
+ meta?: string[];
16
+ }
17
+
18
+ export function renderStatusLine(options: StatusLineOptions, theme: Theme): string {
19
+ const icon = options.icon ? getStateIcon(options.icon, theme, options.spinnerFrame) : "";
20
+ const titleColor = options.titleColor ?? "accent";
21
+ const title = theme.fg(titleColor, options.title);
22
+ let line = icon ? `${icon} ${title}` : title;
23
+
24
+ if (options.description) {
25
+ line += `: ${theme.fg("muted", options.description)}`;
26
+ }
27
+
28
+ if (options.badge) {
29
+ const { label, color } = options.badge;
30
+ line += ` ${theme.fg(color, `${theme.format.bracketLeft}${label}${theme.format.bracketRight}`)}`;
31
+ }
32
+
33
+ const meta = options.meta?.filter(value => value.trim().length > 0) ?? [];
34
+ if (meta.length > 0) {
35
+ line += ` ${theme.fg("dim", meta.join(theme.sep.dot))}`;
36
+ }
37
+
38
+ return line;
39
+ }