@oh-my-pi/pi-coding-agent 0.1.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 (337) hide show
  1. package/CHANGELOG.md +1629 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +541 -0
  6. package/docs/extension-loading.md +1004 -0
  7. package/docs/hooks.md +867 -0
  8. package/docs/rpc.md +1040 -0
  9. package/docs/sdk.md +994 -0
  10. package/docs/session-tree-plan.md +441 -0
  11. package/docs/session.md +240 -0
  12. package/docs/skills.md +290 -0
  13. package/docs/theme.md +670 -0
  14. package/docs/tree.md +197 -0
  15. package/docs/tui.md +341 -0
  16. package/examples/README.md +21 -0
  17. package/examples/custom-tools/README.md +124 -0
  18. package/examples/custom-tools/hello/index.ts +20 -0
  19. package/examples/custom-tools/question/index.ts +84 -0
  20. package/examples/custom-tools/subagent/README.md +172 -0
  21. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +89 -0
  57. package/src/bun-imports.d.ts +16 -0
  58. package/src/capability/context-file.ts +40 -0
  59. package/src/capability/extension.ts +48 -0
  60. package/src/capability/hook.ts +40 -0
  61. package/src/capability/index.ts +616 -0
  62. package/src/capability/instruction.ts +37 -0
  63. package/src/capability/mcp.ts +52 -0
  64. package/src/capability/prompt.ts +35 -0
  65. package/src/capability/rule.ts +56 -0
  66. package/src/capability/settings.ts +35 -0
  67. package/src/capability/skill.ts +49 -0
  68. package/src/capability/slash-command.ts +40 -0
  69. package/src/capability/system-prompt.ts +35 -0
  70. package/src/capability/tool.ts +38 -0
  71. package/src/capability/types.ts +166 -0
  72. package/src/cli/args.ts +259 -0
  73. package/src/cli/file-processor.ts +121 -0
  74. package/src/cli/list-models.ts +104 -0
  75. package/src/cli/plugin-cli.ts +661 -0
  76. package/src/cli/session-picker.ts +41 -0
  77. package/src/cli/update-cli.ts +274 -0
  78. package/src/cli.ts +10 -0
  79. package/src/config.ts +391 -0
  80. package/src/core/agent-session.ts +2178 -0
  81. package/src/core/auth-storage.ts +258 -0
  82. package/src/core/bash-executor.ts +197 -0
  83. package/src/core/compaction/branch-summarization.ts +315 -0
  84. package/src/core/compaction/compaction.ts +664 -0
  85. package/src/core/compaction/index.ts +7 -0
  86. package/src/core/compaction/utils.ts +153 -0
  87. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  88. package/src/core/custom-commands/index.ts +15 -0
  89. package/src/core/custom-commands/loader.ts +226 -0
  90. package/src/core/custom-commands/types.ts +112 -0
  91. package/src/core/custom-tools/index.ts +22 -0
  92. package/src/core/custom-tools/loader.ts +248 -0
  93. package/src/core/custom-tools/types.ts +185 -0
  94. package/src/core/custom-tools/wrapper.ts +29 -0
  95. package/src/core/exec.ts +139 -0
  96. package/src/core/export-html/index.ts +159 -0
  97. package/src/core/export-html/template.css +774 -0
  98. package/src/core/export-html/template.generated.ts +2 -0
  99. package/src/core/export-html/template.html +45 -0
  100. package/src/core/export-html/template.js +1185 -0
  101. package/src/core/export-html/template.macro.ts +24 -0
  102. package/src/core/file-mentions.ts +54 -0
  103. package/src/core/hooks/index.ts +16 -0
  104. package/src/core/hooks/loader.ts +288 -0
  105. package/src/core/hooks/runner.ts +434 -0
  106. package/src/core/hooks/tool-wrapper.ts +98 -0
  107. package/src/core/hooks/types.ts +770 -0
  108. package/src/core/index.ts +53 -0
  109. package/src/core/logger.ts +112 -0
  110. package/src/core/mcp/client.ts +185 -0
  111. package/src/core/mcp/config.ts +248 -0
  112. package/src/core/mcp/index.ts +45 -0
  113. package/src/core/mcp/loader.ts +99 -0
  114. package/src/core/mcp/manager.ts +235 -0
  115. package/src/core/mcp/tool-bridge.ts +156 -0
  116. package/src/core/mcp/transports/http.ts +316 -0
  117. package/src/core/mcp/transports/index.ts +6 -0
  118. package/src/core/mcp/transports/stdio.ts +252 -0
  119. package/src/core/mcp/types.ts +228 -0
  120. package/src/core/messages.ts +211 -0
  121. package/src/core/model-registry.ts +334 -0
  122. package/src/core/model-resolver.ts +494 -0
  123. package/src/core/plugins/doctor.ts +67 -0
  124. package/src/core/plugins/index.ts +38 -0
  125. package/src/core/plugins/installer.ts +189 -0
  126. package/src/core/plugins/loader.ts +339 -0
  127. package/src/core/plugins/manager.ts +672 -0
  128. package/src/core/plugins/parser.ts +105 -0
  129. package/src/core/plugins/paths.ts +37 -0
  130. package/src/core/plugins/types.ts +190 -0
  131. package/src/core/sdk.ts +900 -0
  132. package/src/core/session-manager.ts +1837 -0
  133. package/src/core/settings-manager.ts +860 -0
  134. package/src/core/skills.ts +352 -0
  135. package/src/core/slash-commands.ts +132 -0
  136. package/src/core/system-prompt.ts +442 -0
  137. package/src/core/timings.ts +25 -0
  138. package/src/core/title-generator.ts +110 -0
  139. package/src/core/tools/ask.ts +193 -0
  140. package/src/core/tools/bash-interceptor.ts +120 -0
  141. package/src/core/tools/bash.ts +91 -0
  142. package/src/core/tools/context.ts +32 -0
  143. package/src/core/tools/edit-diff.ts +487 -0
  144. package/src/core/tools/edit.ts +140 -0
  145. package/src/core/tools/exa/company.ts +59 -0
  146. package/src/core/tools/exa/index.ts +63 -0
  147. package/src/core/tools/exa/linkedin.ts +59 -0
  148. package/src/core/tools/exa/mcp-client.ts +368 -0
  149. package/src/core/tools/exa/render.ts +200 -0
  150. package/src/core/tools/exa/researcher.ts +90 -0
  151. package/src/core/tools/exa/search.ts +338 -0
  152. package/src/core/tools/exa/types.ts +167 -0
  153. package/src/core/tools/exa/websets.ts +248 -0
  154. package/src/core/tools/find.ts +244 -0
  155. package/src/core/tools/grep.ts +584 -0
  156. package/src/core/tools/index.ts +283 -0
  157. package/src/core/tools/ls.ts +142 -0
  158. package/src/core/tools/lsp/client.ts +767 -0
  159. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  160. package/src/core/tools/lsp/clients/index.ts +49 -0
  161. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  162. package/src/core/tools/lsp/config.ts +845 -0
  163. package/src/core/tools/lsp/edits.ts +110 -0
  164. package/src/core/tools/lsp/index.ts +1364 -0
  165. package/src/core/tools/lsp/render.ts +560 -0
  166. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  167. package/src/core/tools/lsp/types.ts +495 -0
  168. package/src/core/tools/lsp/utils.ts +526 -0
  169. package/src/core/tools/notebook.ts +182 -0
  170. package/src/core/tools/output.ts +198 -0
  171. package/src/core/tools/path-utils.ts +61 -0
  172. package/src/core/tools/read.ts +507 -0
  173. package/src/core/tools/renderers.ts +820 -0
  174. package/src/core/tools/review.ts +275 -0
  175. package/src/core/tools/rulebook.ts +124 -0
  176. package/src/core/tools/task/agents.ts +158 -0
  177. package/src/core/tools/task/artifacts.ts +114 -0
  178. package/src/core/tools/task/commands.ts +157 -0
  179. package/src/core/tools/task/discovery.ts +217 -0
  180. package/src/core/tools/task/executor.ts +531 -0
  181. package/src/core/tools/task/index.ts +548 -0
  182. package/src/core/tools/task/model-resolver.ts +176 -0
  183. package/src/core/tools/task/parallel.ts +38 -0
  184. package/src/core/tools/task/render.ts +502 -0
  185. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  186. package/src/core/tools/task/types.ts +142 -0
  187. package/src/core/tools/truncate.ts +265 -0
  188. package/src/core/tools/web-fetch.ts +2511 -0
  189. package/src/core/tools/web-search/auth.ts +199 -0
  190. package/src/core/tools/web-search/index.ts +583 -0
  191. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  192. package/src/core/tools/web-search/providers/exa.ts +196 -0
  193. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  194. package/src/core/tools/web-search/render.ts +372 -0
  195. package/src/core/tools/web-search/types.ts +180 -0
  196. package/src/core/tools/write.ts +63 -0
  197. package/src/core/ttsr.ts +211 -0
  198. package/src/core/utils.ts +187 -0
  199. package/src/discovery/agents-md.ts +75 -0
  200. package/src/discovery/builtin.ts +647 -0
  201. package/src/discovery/claude.ts +623 -0
  202. package/src/discovery/cline.ts +104 -0
  203. package/src/discovery/codex.ts +571 -0
  204. package/src/discovery/cursor.ts +266 -0
  205. package/src/discovery/gemini.ts +368 -0
  206. package/src/discovery/github.ts +120 -0
  207. package/src/discovery/helpers.test.ts +127 -0
  208. package/src/discovery/helpers.ts +249 -0
  209. package/src/discovery/index.ts +84 -0
  210. package/src/discovery/mcp-json.ts +127 -0
  211. package/src/discovery/vscode.ts +99 -0
  212. package/src/discovery/windsurf.ts +219 -0
  213. package/src/index.ts +192 -0
  214. package/src/main.ts +507 -0
  215. package/src/migrations.ts +156 -0
  216. package/src/modes/cleanup.ts +23 -0
  217. package/src/modes/index.ts +48 -0
  218. package/src/modes/interactive/components/armin.ts +382 -0
  219. package/src/modes/interactive/components/assistant-message.ts +86 -0
  220. package/src/modes/interactive/components/bash-execution.ts +199 -0
  221. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  222. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  223. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  224. package/src/modes/interactive/components/custom-editor.ts +122 -0
  225. package/src/modes/interactive/components/diff.ts +147 -0
  226. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  227. package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
  228. package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
  229. package/src/modes/interactive/components/extensions/index.ts +9 -0
  230. package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
  231. package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
  232. package/src/modes/interactive/components/extensions/types.ts +191 -0
  233. package/src/modes/interactive/components/hook-editor.ts +117 -0
  234. package/src/modes/interactive/components/hook-input.ts +64 -0
  235. package/src/modes/interactive/components/hook-message.ts +96 -0
  236. package/src/modes/interactive/components/hook-selector.ts +91 -0
  237. package/src/modes/interactive/components/model-selector.ts +560 -0
  238. package/src/modes/interactive/components/oauth-selector.ts +136 -0
  239. package/src/modes/interactive/components/plugin-settings.ts +481 -0
  240. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  241. package/src/modes/interactive/components/session-selector.ts +220 -0
  242. package/src/modes/interactive/components/settings-defs.ts +597 -0
  243. package/src/modes/interactive/components/settings-selector.ts +545 -0
  244. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  245. package/src/modes/interactive/components/status-line/index.ts +4 -0
  246. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  247. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  248. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  249. package/src/modes/interactive/components/status-line/types.ts +81 -0
  250. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  251. package/src/modes/interactive/components/status-line.ts +384 -0
  252. package/src/modes/interactive/components/theme-selector.ts +62 -0
  253. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  254. package/src/modes/interactive/components/tool-execution.ts +946 -0
  255. package/src/modes/interactive/components/tree-selector.ts +877 -0
  256. package/src/modes/interactive/components/ttsr-notification.ts +82 -0
  257. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  258. package/src/modes/interactive/components/user-message.ts +18 -0
  259. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  260. package/src/modes/interactive/components/welcome.ts +228 -0
  261. package/src/modes/interactive/interactive-mode.ts +2669 -0
  262. package/src/modes/interactive/theme/dark.json +102 -0
  263. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  264. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  265. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  266. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  267. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  268. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  269. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  270. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  271. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  272. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  273. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  274. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  275. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  276. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  277. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  278. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  279. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  280. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  281. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  282. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  283. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  284. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  285. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  286. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  287. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  288. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  289. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  290. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  291. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  292. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  293. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  294. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  295. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  296. package/src/modes/interactive/theme/light.json +99 -0
  297. package/src/modes/interactive/theme/theme-schema.json +424 -0
  298. package/src/modes/interactive/theme/theme.ts +2211 -0
  299. package/src/modes/print-mode.ts +163 -0
  300. package/src/modes/rpc/rpc-client.ts +527 -0
  301. package/src/modes/rpc/rpc-mode.ts +494 -0
  302. package/src/modes/rpc/rpc-types.ts +203 -0
  303. package/src/prompts/architect-plan.md +10 -0
  304. package/src/prompts/branch-summary-preamble.md +3 -0
  305. package/src/prompts/branch-summary.md +28 -0
  306. package/src/prompts/browser.md +71 -0
  307. package/src/prompts/compaction-summary.md +34 -0
  308. package/src/prompts/compaction-turn-prefix.md +16 -0
  309. package/src/prompts/compaction-update-summary.md +41 -0
  310. package/src/prompts/explore.md +82 -0
  311. package/src/prompts/implement-with-critic.md +11 -0
  312. package/src/prompts/implement.md +11 -0
  313. package/src/prompts/init.md +30 -0
  314. package/src/prompts/plan.md +54 -0
  315. package/src/prompts/reviewer.md +81 -0
  316. package/src/prompts/summarization-system.md +3 -0
  317. package/src/prompts/system-prompt.md +27 -0
  318. package/src/prompts/task.md +56 -0
  319. package/src/prompts/title-system.md +8 -0
  320. package/src/prompts/tools/ask.md +24 -0
  321. package/src/prompts/tools/bash.md +23 -0
  322. package/src/prompts/tools/edit.md +9 -0
  323. package/src/prompts/tools/find.md +6 -0
  324. package/src/prompts/tools/grep.md +12 -0
  325. package/src/prompts/tools/lsp.md +14 -0
  326. package/src/prompts/tools/output.md +23 -0
  327. package/src/prompts/tools/read.md +25 -0
  328. package/src/prompts/tools/web-fetch.md +8 -0
  329. package/src/prompts/tools/web-search.md +10 -0
  330. package/src/prompts/tools/write.md +10 -0
  331. package/src/utils/changelog.ts +99 -0
  332. package/src/utils/clipboard.ts +265 -0
  333. package/src/utils/fuzzy.ts +108 -0
  334. package/src/utils/mime.ts +30 -0
  335. package/src/utils/shell-snapshot.ts +218 -0
  336. package/src/utils/shell.ts +364 -0
  337. package/src/utils/tools-manager.ts +265 -0
@@ -0,0 +1,502 @@
1
+ /**
2
+ * TUI rendering for task tool.
3
+ *
4
+ * Provides renderCall and renderResult functions for displaying
5
+ * task execution in the terminal UI.
6
+ */
7
+
8
+ import path from "node:path";
9
+ import type { Component } from "@oh-my-pi/pi-tui";
10
+ import { Container, Text } from "@oh-my-pi/pi-tui";
11
+ import type { Theme } from "../../../modes/interactive/theme/theme";
12
+ import type { RenderResultOptions } from "../../custom-tools/types";
13
+ import type { ReportFindingDetails, SubmitReviewDetails } from "../review";
14
+ import { subprocessToolRegistry } from "./subprocess-tool-registry";
15
+ import type { AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types";
16
+
17
+ /** Priority labels for review findings */
18
+ const PRIORITY_LABELS: Record<number, string> = {
19
+ 0: "P0",
20
+ 1: "P1",
21
+ 2: "P2",
22
+ 3: "P3",
23
+ };
24
+
25
+ /**
26
+ * Format token count for display (e.g., 1.5k, 25k).
27
+ */
28
+ function formatTokens(tokens: number): string {
29
+ if (tokens >= 1000) {
30
+ return `${(tokens / 1000).toFixed(1)}k`;
31
+ }
32
+ return String(tokens);
33
+ }
34
+
35
+ /**
36
+ * Format duration for display.
37
+ */
38
+ export function formatDuration(ms: number): string {
39
+ if (ms < 1000) return `${ms}ms`;
40
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
41
+ return `${(ms / 60000).toFixed(1)}m`;
42
+ }
43
+
44
+ /**
45
+ * Truncate text to max length with ellipsis.
46
+ */
47
+ function truncate(text: string, maxLen: number, ellipsis: string): string {
48
+ if (text.length <= maxLen) return text;
49
+ const sliceLen = Math.max(0, maxLen - ellipsis.length);
50
+ return `${text.slice(0, sliceLen)}${ellipsis}`;
51
+ }
52
+
53
+ /**
54
+ * Get status icon for agent state.
55
+ * For running status, uses animated spinner if spinnerFrame is provided.
56
+ */
57
+ function getStatusIcon(status: AgentProgress["status"], theme: Theme, spinnerFrame?: number): string {
58
+ switch (status) {
59
+ case "pending":
60
+ return theme.status.pending;
61
+ case "running": {
62
+ // Use animated spinner if frame is provided, otherwise static icon
63
+ if (spinnerFrame === undefined) return theme.status.running;
64
+ const frames = theme.spinnerFrames;
65
+ return frames[spinnerFrame % frames.length];
66
+ }
67
+ case "completed":
68
+ return theme.status.success;
69
+ case "failed":
70
+ return theme.status.error;
71
+ case "aborted":
72
+ return theme.status.aborted;
73
+ }
74
+ }
75
+
76
+ function formatBadge(label: string, color: "success" | "error" | "warning" | "accent" | "muted", theme: Theme): string {
77
+ const left = theme.format.bracketLeft;
78
+ const right = theme.format.bracketRight;
79
+ return theme.fg(color, `${left}${label}${right}`);
80
+ }
81
+
82
+ function formatFindingSummary(findings: ReportFindingDetails[], theme: Theme): string {
83
+ if (findings.length === 0) return theme.fg("dim", "Findings: none");
84
+
85
+ const counts = new Map<number, number>();
86
+ for (const finding of findings) {
87
+ counts.set(finding.priority, (counts.get(finding.priority) ?? 0) + 1);
88
+ }
89
+
90
+ const parts: string[] = [];
91
+ for (const priority of [0, 1, 2, 3]) {
92
+ const label = PRIORITY_LABELS[priority] ?? "P?";
93
+ const color = priority === 0 ? "error" : priority === 1 ? "warning" : "muted";
94
+ const count = counts.get(priority) ?? 0;
95
+ parts.push(theme.fg(color, `${label}:${count}`));
96
+ }
97
+
98
+ return `${theme.fg("dim", "Findings:")} ${parts.join(theme.sep.dot)}`;
99
+ }
100
+
101
+ function renderOutputSection(
102
+ output: string,
103
+ continuePrefix: string,
104
+ expanded: boolean,
105
+ theme: Theme,
106
+ maxCollapsed = 3,
107
+ maxExpanded = 10,
108
+ ): string[] {
109
+ const lines: string[] = [];
110
+ const outputLines = output.split("\n").filter((line) => line.trim());
111
+ if (outputLines.length === 0) return lines;
112
+
113
+ lines.push(`${continuePrefix}${theme.fg("dim", "Output")}`);
114
+
115
+ const previewCount = expanded ? maxExpanded : maxCollapsed;
116
+ for (const line of outputLines.slice(0, previewCount)) {
117
+ lines.push(`${continuePrefix} ${theme.fg("dim", truncate(line, 70, theme.format.ellipsis))}`);
118
+ }
119
+
120
+ if (outputLines.length > previewCount) {
121
+ lines.push(
122
+ `${continuePrefix} ${theme.fg(
123
+ "dim",
124
+ `${theme.format.ellipsis} ${outputLines.length - previewCount} more lines`,
125
+ )}`,
126
+ );
127
+ }
128
+
129
+ return lines;
130
+ }
131
+
132
+ /**
133
+ * Render the tool call arguments.
134
+ */
135
+ export function renderCall(args: TaskParams, theme: Theme): Component {
136
+ const label = theme.fg("toolTitle", theme.bold("task"));
137
+
138
+ if (args.tasks.length === 1) {
139
+ // Single task - show agent and task preview
140
+ const task = args.tasks[0];
141
+ const taskPreview = truncate(task.task, 60, theme.format.ellipsis);
142
+ return new Text(`${label} ${theme.fg("accent", task.agent)}: ${theme.fg("muted", taskPreview)}`, 0, 0);
143
+ }
144
+
145
+ // Multiple tasks - show count and agent names
146
+ const agents = args.tasks.map((t) => t.agent).join(", ");
147
+ return new Text(
148
+ `${label} ${theme.fg("muted", `${args.tasks.length} agents: ${truncate(agents, 50, theme.format.ellipsis)}`)}`,
149
+ 0,
150
+ 0,
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Render streaming progress for a single agent.
156
+ */
157
+ function renderAgentProgress(
158
+ progress: AgentProgress,
159
+ isLast: boolean,
160
+ expanded: boolean,
161
+ theme: Theme,
162
+ spinnerFrame?: number,
163
+ ): string[] {
164
+ const lines: string[] = [];
165
+ const prefix = isLast
166
+ ? `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal}`
167
+ : `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal}`;
168
+ const continuePrefix = isLast ? " " : `${theme.boxSharp.vertical} `;
169
+
170
+ const icon = getStatusIcon(progress.status, theme, spinnerFrame);
171
+ const iconColor =
172
+ progress.status === "completed"
173
+ ? "success"
174
+ : progress.status === "failed" || progress.status === "aborted"
175
+ ? "error"
176
+ : "accent";
177
+
178
+ // Main status line - include index for Output tool ID derivation
179
+ const agentId = `${progress.agent}(${progress.index})`;
180
+ let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", agentId)}`;
181
+
182
+ // Only show badge for non-running states (spinner already indicates running)
183
+ if (progress.status !== "running") {
184
+ const statusLabel =
185
+ progress.status === "completed"
186
+ ? "done"
187
+ : progress.status === "failed"
188
+ ? "failed"
189
+ : progress.status === "aborted"
190
+ ? "aborted"
191
+ : "pending";
192
+ statusLine += ` ${formatBadge(statusLabel, iconColor, theme)}`;
193
+ }
194
+
195
+ if (progress.status === "running") {
196
+ const taskPreview = truncate(progress.task, 40, theme.format.ellipsis);
197
+ statusLine += ` ${theme.fg("muted", taskPreview)}`;
198
+ statusLine += `${theme.sep.dot}${theme.fg("dim", `${progress.toolCount} tools`)}`;
199
+ if (progress.tokens > 0) {
200
+ statusLine += `${theme.sep.dot}${theme.fg("dim", `${formatTokens(progress.tokens)} tokens`)}`;
201
+ }
202
+ } else if (progress.status === "completed") {
203
+ statusLine += `${theme.sep.dot}${theme.fg("dim", `${progress.toolCount} tools`)}`;
204
+ statusLine += `${theme.sep.dot}${theme.fg("dim", `${formatTokens(progress.tokens)} tokens`)}`;
205
+ }
206
+
207
+ lines.push(statusLine);
208
+
209
+ // Current tool (if running)
210
+ if (progress.status === "running" && progress.currentTool) {
211
+ let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", progress.currentTool)}`;
212
+ if (progress.currentToolArgs) {
213
+ toolLine += `: ${theme.fg("dim", truncate(progress.currentToolArgs, 40, theme.format.ellipsis))}`;
214
+ }
215
+ if (progress.currentToolStartMs) {
216
+ const elapsed = Date.now() - progress.currentToolStartMs;
217
+ if (elapsed > 5000) {
218
+ toolLine += `${theme.sep.dot}${theme.fg("warning", formatDuration(elapsed))}`;
219
+ }
220
+ }
221
+ lines.push(toolLine);
222
+ }
223
+
224
+ // Render extracted tool data inline (e.g., review findings)
225
+ if (progress.extractedToolData) {
226
+ for (const [toolName, dataArray] of Object.entries(progress.extractedToolData)) {
227
+ const handler = subprocessToolRegistry.getHandler(toolName);
228
+ if (handler?.renderInline) {
229
+ // Show last few items inline
230
+ const recentData = (dataArray as unknown[]).slice(-3);
231
+ for (const data of recentData) {
232
+ const component = handler.renderInline(data, theme);
233
+ if (component instanceof Text) {
234
+ lines.push(`${continuePrefix}${component.getText()}`);
235
+ }
236
+ }
237
+ if (dataArray.length > 3) {
238
+ lines.push(
239
+ `${continuePrefix}${theme.fg("dim", `${theme.format.ellipsis} ${dataArray.length - 3} more`)}`,
240
+ );
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ // Expanded view: recent output and tools
247
+ if (expanded && progress.status === "running") {
248
+ const output = progress.recentOutput.join("\n");
249
+ lines.push(...renderOutputSection(output, continuePrefix, true, theme, 2, 6));
250
+ }
251
+
252
+ return lines;
253
+ }
254
+
255
+ /**
256
+ * Render review result with combined verdict + findings in tree structure.
257
+ */
258
+ function renderReviewResult(
259
+ summary: SubmitReviewDetails,
260
+ findings: ReportFindingDetails[],
261
+ continuePrefix: string,
262
+ expanded: boolean,
263
+ theme: Theme,
264
+ ): string[] {
265
+ const lines: string[] = [];
266
+
267
+ // Verdict line
268
+ const verdictColor = summary.overall_correctness === "correct" ? "success" : "error";
269
+ const verdictIcon = summary.overall_correctness === "correct" ? theme.status.success : theme.status.error;
270
+ lines.push(
271
+ `${continuePrefix}${theme.fg(verdictColor, verdictIcon)} Patch is ${theme.fg(verdictColor, summary.overall_correctness)} ${theme.fg("dim", `(${(summary.confidence * 100).toFixed(0)}% confidence)`)}`,
272
+ );
273
+
274
+ // Explanation preview (first ~80 chars when collapsed, full when expanded)
275
+ if (summary.explanation) {
276
+ if (expanded) {
277
+ lines.push(`${continuePrefix}${theme.fg("dim", "Summary")}`);
278
+ const explanationLines = summary.explanation.split("\n");
279
+ for (const line of explanationLines) {
280
+ lines.push(`${continuePrefix} ${theme.fg("dim", line)}`);
281
+ }
282
+ } else {
283
+ // Preview: first sentence or ~100 chars
284
+ const preview = truncate(`${summary.explanation.split(/[.!?]/)[0]}.`, 100, theme.format.ellipsis);
285
+ lines.push(`${continuePrefix}${theme.fg("dim", `Summary: ${preview}`)}`);
286
+ }
287
+ }
288
+
289
+ // Findings summary + list
290
+ lines.push(`${continuePrefix}${formatFindingSummary(findings, theme)}`);
291
+
292
+ if (findings.length > 0) {
293
+ lines.push(`${continuePrefix}`); // Spacing
294
+ lines.push(...renderFindings(findings, continuePrefix, expanded, theme));
295
+ }
296
+
297
+ return lines;
298
+ }
299
+
300
+ /**
301
+ * Render review findings list (used with and without submit_review).
302
+ */
303
+ function renderFindings(
304
+ findings: ReportFindingDetails[],
305
+ continuePrefix: string,
306
+ expanded: boolean,
307
+ theme: Theme,
308
+ ): string[] {
309
+ const lines: string[] = [];
310
+ const displayCount = expanded ? findings.length : Math.min(3, findings.length);
311
+
312
+ for (let i = 0; i < displayCount; i++) {
313
+ const finding = findings[i];
314
+ const isLastFinding = i === displayCount - 1 && (expanded || findings.length <= 3);
315
+ const findingPrefix = isLastFinding
316
+ ? `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal}`
317
+ : `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal}`;
318
+ const findingContinue = isLastFinding ? " " : `${theme.boxSharp.vertical} `;
319
+
320
+ const priority = PRIORITY_LABELS[finding.priority] ?? "P?";
321
+ const color = finding.priority === 0 ? "error" : finding.priority === 1 ? "warning" : "muted";
322
+ const titleText = finding.title.replace(/^\[P\d\]\s*/, "");
323
+ const loc = `${path.basename(finding.file_path)}:${finding.line_start}`;
324
+
325
+ lines.push(
326
+ `${continuePrefix}${findingPrefix} ${theme.fg(color, `[${priority}]`)} ${titleText} ${theme.fg("dim", loc)}`,
327
+ );
328
+
329
+ // Show body when expanded
330
+ if (expanded && finding.body) {
331
+ // Wrap body text
332
+ const bodyLines = finding.body.split("\n");
333
+ for (const bodyLine of bodyLines) {
334
+ lines.push(`${continuePrefix}${findingContinue}${theme.fg("dim", bodyLine)}`);
335
+ }
336
+ }
337
+ }
338
+
339
+ if (!expanded && findings.length > 3) {
340
+ lines.push(
341
+ `${continuePrefix}${theme.fg("dim", `${theme.format.ellipsis} ${findings.length - 3} more findings`)}`,
342
+ );
343
+ }
344
+
345
+ return lines;
346
+ }
347
+
348
+ /**
349
+ * Render final result for a single agent.
350
+ */
351
+ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: boolean, theme: Theme): string[] {
352
+ const lines: string[] = [];
353
+ const prefix = isLast
354
+ ? `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal}`
355
+ : `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal}`;
356
+ const continuePrefix = isLast ? " " : `${theme.boxSharp.vertical} `;
357
+
358
+ const aborted = result.aborted ?? false;
359
+ const success = !aborted && result.exitCode === 0;
360
+ const icon = aborted ? theme.status.aborted : success ? theme.status.success : theme.status.error;
361
+ const iconColor = success ? "success" : "error";
362
+ const statusText = aborted ? "aborted" : success ? "done" : "failed";
363
+
364
+ // Main status line - include index for Output tool ID derivation
365
+ const agentId = `${result.agent}(${result.index})`;
366
+ let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", agentId)} ${formatBadge(statusText, iconColor, theme)}`;
367
+ if (result.tokens > 0) {
368
+ statusLine += `${theme.sep.dot}${theme.fg("dim", `${formatTokens(result.tokens)} tokens`)}`;
369
+ }
370
+ statusLine += `${theme.sep.dot}${theme.fg("dim", formatDuration(result.durationMs))}`;
371
+
372
+ if (result.truncated) {
373
+ statusLine += ` ${theme.fg("warning", "[truncated]")}`;
374
+ }
375
+
376
+ lines.push(statusLine);
377
+
378
+ // Check for review result (submit_review + report_finding)
379
+ const submitReviewData = result.extractedToolData?.submit_review as SubmitReviewDetails[] | undefined;
380
+ const reportFindingData = result.extractedToolData?.report_finding as ReportFindingDetails[] | undefined;
381
+
382
+ if (submitReviewData && submitReviewData.length > 0) {
383
+ // Use combined review renderer
384
+ const summary = submitReviewData[submitReviewData.length - 1];
385
+ const findings = reportFindingData ?? [];
386
+ lines.push(...renderReviewResult(summary, findings, continuePrefix, expanded, theme));
387
+ return lines;
388
+ }
389
+ if (reportFindingData && reportFindingData.length > 0) {
390
+ lines.push(
391
+ `${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg("dim", "Review summary missing (submit_review not called)")}`,
392
+ );
393
+ lines.push(`${continuePrefix}${formatFindingSummary(reportFindingData, theme)}`);
394
+ lines.push(`${continuePrefix}`); // Spacing
395
+ lines.push(...renderFindings(reportFindingData, continuePrefix, expanded, theme));
396
+ return lines;
397
+ }
398
+
399
+ // Check for extracted tool data with custom renderers (skip review tools)
400
+ let hasCustomRendering = false;
401
+ if (result.extractedToolData) {
402
+ for (const [toolName, dataArray] of Object.entries(result.extractedToolData)) {
403
+ // Skip review tools - handled above
404
+ if (toolName === "submit_review" || toolName === "report_finding") continue;
405
+
406
+ const handler = subprocessToolRegistry.getHandler(toolName);
407
+ if (handler?.renderFinal && (dataArray as unknown[]).length > 0) {
408
+ hasCustomRendering = true;
409
+ const component = handler.renderFinal(dataArray as unknown[], theme, expanded);
410
+ if (component instanceof Text) {
411
+ // Prefix each line with continuePrefix
412
+ const text = component.getText();
413
+ for (const line of text.split("\n")) {
414
+ if (line.trim()) {
415
+ lines.push(`${continuePrefix}${line}`);
416
+ }
417
+ }
418
+ } else if (component instanceof Container) {
419
+ // For containers, render each child
420
+ for (const child of (component as Container).children) {
421
+ if (child instanceof Text) {
422
+ lines.push(`${continuePrefix}${child.getText()}`);
423
+ }
424
+ }
425
+ }
426
+ }
427
+ }
428
+ }
429
+
430
+ // Fallback to output preview if no custom rendering
431
+ if (!hasCustomRendering) {
432
+ lines.push(...renderOutputSection(result.output, continuePrefix, expanded, theme, 3, 12));
433
+ }
434
+
435
+ // Error message
436
+ if (result.error && !success) {
437
+ lines.push(`${continuePrefix}${theme.fg("error", truncate(result.error, 70, theme.format.ellipsis))}`);
438
+ }
439
+
440
+ return lines;
441
+ }
442
+
443
+ /**
444
+ * Render the tool result.
445
+ */
446
+ export function renderResult(
447
+ result: { content: Array<{ type: string; text?: string }>; details?: TaskToolDetails },
448
+ options: RenderResultOptions,
449
+ theme: Theme,
450
+ ): Component {
451
+ const { expanded, isPartial, spinnerFrame } = options;
452
+ const details = result.details;
453
+
454
+ if (!details) {
455
+ // Fallback to simple text
456
+ const text = result.content.find((c) => c.type === "text")?.text || "";
457
+ return new Text(theme.fg("dim", truncate(text, 100, theme.format.ellipsis)), 0, 0);
458
+ }
459
+
460
+ const lines: string[] = [];
461
+
462
+ if (isPartial && details.progress) {
463
+ // Streaming progress view
464
+ details.progress.forEach((progress, i) => {
465
+ const isLast = i === details.progress!.length - 1;
466
+ lines.push(...renderAgentProgress(progress, isLast, expanded, theme, spinnerFrame));
467
+ });
468
+ } else if (details.results.length > 0) {
469
+ // Final results view
470
+ details.results.forEach((res, i) => {
471
+ const isLast = i === details.results.length - 1;
472
+ lines.push(...renderAgentResult(res, isLast, expanded, theme));
473
+ });
474
+
475
+ // Summary line
476
+ const abortedCount = details.results.filter((r) => r.aborted).length;
477
+ const successCount = details.results.filter((r) => !r.aborted && r.exitCode === 0).length;
478
+ const failCount = details.results.length - successCount - abortedCount;
479
+ let summary = `\n${theme.fg("dim", "Total:")} `;
480
+ if (abortedCount > 0) {
481
+ summary += theme.fg("error", `${abortedCount} aborted`);
482
+ if (successCount > 0 || failCount > 0) summary += ", ";
483
+ }
484
+ if (successCount > 0) {
485
+ summary += theme.fg("success", `${successCount} succeeded`);
486
+ if (failCount > 0) summary += ", ";
487
+ }
488
+ if (failCount > 0) {
489
+ summary += theme.fg("error", `${failCount} failed`);
490
+ }
491
+ summary += `${theme.sep.dot}${theme.fg("dim", formatDuration(details.totalDurationMs))}`;
492
+ lines.push(summary);
493
+
494
+ // Artifacts suppressed from user view - available via session file
495
+ }
496
+
497
+ if (lines.length === 0) {
498
+ return new Text(theme.fg("dim", "No results"), 0, 0);
499
+ }
500
+
501
+ return new Text(lines.join("\n"), 0, 0);
502
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Registry for handling tool events from subprocess agents.
3
+ *
4
+ * Tools can register handlers to:
5
+ * - Extract structured data from their execution results
6
+ * - Trigger subprocess termination on completion
7
+ * - Provide custom rendering for realtime/final display
8
+ */
9
+
10
+ import type { Component } from "@oh-my-pi/pi-tui";
11
+ import type { Theme } from "../../../modes/interactive/theme/theme";
12
+
13
+ /** Event from subprocess tool execution (parsed from JSONL) */
14
+ export interface SubprocessToolEvent {
15
+ toolName: string;
16
+ toolCallId: string;
17
+ args?: Record<string, unknown>;
18
+ result?: {
19
+ content: Array<{ type: string; text?: string }>;
20
+ details?: unknown;
21
+ };
22
+ isError?: boolean;
23
+ }
24
+
25
+ /** Handler for subprocess tool events */
26
+ export interface SubprocessToolHandler<TData = unknown> {
27
+ /**
28
+ * Extract structured data from tool result.
29
+ * Extracted data is accumulated in progress.extractedToolData[toolName][].
30
+ */
31
+ extractData?: (event: SubprocessToolEvent) => TData | undefined;
32
+
33
+ /**
34
+ * Whether this tool's completion should terminate the subprocess.
35
+ * Return true to send SIGTERM after the tool completes.
36
+ */
37
+ shouldTerminate?: (event: SubprocessToolEvent) => boolean;
38
+
39
+ /**
40
+ * Render a single data item inline during streaming progress.
41
+ * Called for each tool execution end event.
42
+ */
43
+ renderInline?: (data: TData, theme: Theme) => Component;
44
+
45
+ /**
46
+ * Render accumulated data in the final result view.
47
+ * Called once with all accumulated data for this tool.
48
+ */
49
+ renderFinal?: (allData: TData[], theme: Theme, expanded: boolean) => Component;
50
+ }
51
+
52
+ /** Registry for subprocess tool handlers */
53
+ class SubprocessToolRegistryImpl {
54
+ private handlers = new Map<string, SubprocessToolHandler>();
55
+
56
+ /**
57
+ * Register a handler for a tool's subprocess events.
58
+ */
59
+ register<T>(toolName: string, handler: SubprocessToolHandler<T>): void {
60
+ this.handlers.set(toolName, handler as SubprocessToolHandler);
61
+ }
62
+
63
+ /**
64
+ * Get the handler for a tool, if registered.
65
+ */
66
+ getHandler(toolName: string): SubprocessToolHandler | undefined {
67
+ return this.handlers.get(toolName);
68
+ }
69
+
70
+ /**
71
+ * Check if a tool has a registered handler.
72
+ */
73
+ hasHandler(toolName: string): boolean {
74
+ return this.handlers.has(toolName);
75
+ }
76
+
77
+ /**
78
+ * Get all registered tool names.
79
+ */
80
+ getRegisteredTools(): string[] {
81
+ return Array.from(this.handlers.keys());
82
+ }
83
+ }
84
+
85
+ /** Singleton registry instance */
86
+ export const subprocessToolRegistry = new SubprocessToolRegistryImpl();
87
+
88
+ /** Type helper for extracted tool data in progress/result */
89
+ export type ExtractedToolData = Record<string, unknown[]>;