@oh-my-pi/pi-tui 18.2.3 → 18.2.5

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 (750) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +43 -0
  3. package/dist/types/app-keybindings.d.ts +395 -0
  4. package/dist/types/apps/autoresearch-dashboard.d.ts +43 -0
  5. package/dist/types/apps/autoresearch-data.d.ts +15 -0
  6. package/dist/types/apps/cleanse-board.d.ts +66 -0
  7. package/dist/types/apps/cleanse-picker.d.ts +23 -0
  8. package/dist/types/apps/debug/log-formatting.d.ts +6 -0
  9. package/dist/types/apps/debug/log-viewer.d.ts +81 -0
  10. package/dist/types/apps/debug/protocol-probe.d.ts +42 -0
  11. package/dist/types/apps/debug/raw-sse-buffer.d.ts +58 -0
  12. package/dist/types/apps/debug/raw-sse.d.ts +26 -0
  13. package/dist/types/apps/debug/terminal-info.d.ts +34 -0
  14. package/dist/types/apps/debug/viewer-frame.d.ts +52 -0
  15. package/dist/types/apps/git/avatar.d.ts +9 -0
  16. package/dist/types/apps/git/colors.d.ts +49 -0
  17. package/dist/types/apps/git/diff-pane.d.ts +188 -0
  18. package/dist/types/apps/git/git-tui.d.ts +66 -0
  19. package/dist/types/apps/git/sidebar.d.ts +149 -0
  20. package/dist/types/apps/git/state.d.ts +117 -0
  21. package/dist/types/apps/if-bench-board.d.ts +75 -0
  22. package/dist/types/apps/live-visualizer.d.ts +33 -0
  23. package/dist/types/apps/ps-data.d.ts +51 -0
  24. package/dist/types/apps/ps-top.d.ts +33 -0
  25. package/dist/types/apps/session-picker.d.ts +26 -0
  26. package/dist/types/apps/setup-model-picker.d.ts +13 -0
  27. package/dist/types/apps/standalone-picker.d.ts +42 -0
  28. package/dist/types/autocomplete.d.ts +4 -0
  29. package/dist/types/chat/advisor-message.d.ts +11 -0
  30. package/dist/types/chat/assistant-message.d.ts +106 -0
  31. package/dist/types/chat/background-tan-message.d.ts +9 -0
  32. package/dist/types/chat/bash-execution.d.ts +47 -0
  33. package/dist/types/chat/cache-invalidation-marker.d.ts +50 -0
  34. package/dist/types/chat/chat-transcript-builder.d.ts +50 -0
  35. package/dist/types/chat/collab-prompt-message.d.ts +10 -0
  36. package/dist/types/chat/compaction-summary-message.d.ts +54 -0
  37. package/dist/types/chat/custom-message.d.ts +10 -0
  38. package/dist/types/chat/display-preferences.d.ts +13 -0
  39. package/dist/types/chat/eval-execution.d.ts +29 -0
  40. package/dist/types/chat/execution-shared.d.ts +51 -0
  41. package/dist/types/chat/extension-types.d.ts +28 -0
  42. package/dist/types/chat/hook-message.d.ts +10 -0
  43. package/dist/types/chat/image-loading.d.ts +46 -0
  44. package/dist/types/chat/late-diagnostics-message.d.ts +21 -0
  45. package/dist/types/chat/messages.d.ts +189 -0
  46. package/dist/types/chat/reaction.d.ts +30 -0
  47. package/dist/types/chat/read-target.d.ts +4 -0
  48. package/dist/types/chat/read-tool-group.d.ts +67 -0
  49. package/dist/types/chat/served-model-marker.d.ts +47 -0
  50. package/dist/types/chat/skill-message.d.ts +33 -0
  51. package/dist/types/chat/skill-title-input.d.ts +14 -0
  52. package/dist/types/chat/stripped-tool-calls-placeholder.d.ts +12 -0
  53. package/dist/types/chat/thinking-display.d.ts +12 -0
  54. package/dist/types/chat/todo-reminder.d.ts +13 -0
  55. package/dist/types/chat/tool-execution.d.ts +138 -0
  56. package/dist/types/chat/transcript-browser.d.ts +93 -0
  57. package/dist/types/chat/transcript-entry.d.ts +45 -0
  58. package/dist/types/chat/transcript-outline.d.ts +86 -0
  59. package/dist/types/chat/transcript-render-helpers.d.ts +92 -0
  60. package/dist/types/chat/ttsr-notification.d.ts +23 -0
  61. package/dist/types/chat/user-message.d.ts +52 -0
  62. package/dist/types/chrome/chat-block.d.ts +64 -0
  63. package/dist/types/chrome/collab-qrcode.d.ts +28 -0
  64. package/dist/types/chrome/context-thresholds.d.ts +10 -0
  65. package/dist/types/chrome/countdown-timer.d.ts +14 -0
  66. package/dist/types/chrome/diff.d.ts +14 -0
  67. package/dist/types/chrome/dynamic-border.d.ts +16 -0
  68. package/dist/types/chrome/error-block.d.ts +17 -0
  69. package/dist/types/chrome/form-theme.d.ts +3 -0
  70. package/dist/types/chrome/format.d.ts +12 -0
  71. package/dist/types/chrome/index.d.ts +20 -0
  72. package/dist/types/chrome/keybinding-hints.d.ts +40 -0
  73. package/dist/types/chrome/live-board.d.ts +23 -0
  74. package/dist/types/chrome/local-date.d.ts +4 -0
  75. package/dist/types/chrome/message-divider.d.ts +22 -0
  76. package/dist/types/chrome/message-frame.d.ts +52 -0
  77. package/dist/types/chrome/message-notice.d.ts +35 -0
  78. package/dist/types/chrome/overlay-box.d.ts +74 -0
  79. package/dist/types/chrome/qrcode.d.ts +48 -0
  80. package/dist/types/chrome/segment-track.d.ts +27 -0
  81. package/dist/types/chrome/select-list-mouse-routing.d.ts +4 -0
  82. package/dist/types/chrome/selector-helpers.d.ts +52 -0
  83. package/dist/types/chrome/shared.d.ts +5 -0
  84. package/dist/types/chrome/streaming-panel.d.ts +19 -0
  85. package/dist/types/chrome/tool-activity.d.ts +17 -0
  86. package/dist/types/chrome/transcript-container.d.ts +129 -0
  87. package/dist/types/chrome/transcript-status.d.ts +12 -0
  88. package/dist/types/chrome/visual-truncate.d.ts +19 -0
  89. package/dist/types/components/disclosure.d.ts +42 -0
  90. package/dist/types/components/form.d.ts +130 -0
  91. package/dist/types/components/image.d.ts +18 -16
  92. package/dist/types/components/key-value-list.d.ts +24 -0
  93. package/dist/types/components/layout/geometry.d.ts +73 -0
  94. package/dist/types/components/layout/row.d.ts +55 -0
  95. package/dist/types/components/layout/split-pane.d.ts +80 -0
  96. package/dist/types/components/layout/stack.d.ts +50 -0
  97. package/dist/types/components/menu-selection.d.ts +71 -0
  98. package/dist/types/components/metric.d.ts +44 -0
  99. package/dist/types/components/progress-bar.d.ts +36 -0
  100. package/dist/types/components/scroll-view.d.ts +53 -19
  101. package/dist/types/components/scroll-viewport.d.ts +46 -0
  102. package/dist/types/components/section.d.ts +19 -0
  103. package/dist/types/components/select-list.d.ts +46 -2
  104. package/dist/types/components/settings-list.d.ts +20 -0
  105. package/dist/types/components/table.d.ts +32 -0
  106. package/dist/types/components/tree-view.d.ts +130 -0
  107. package/dist/types/components/wizard-step.d.ts +60 -0
  108. package/dist/types/hotkeys-markdown.d.ts +7 -0
  109. package/dist/types/index.d.ts +17 -0
  110. package/dist/types/keybinding-matchers.d.ts +35 -0
  111. package/dist/types/keys.d.ts +2 -2
  112. package/dist/types/lang-from-path.d.ts +9 -0
  113. package/dist/types/overlays/advisor-config.d.ts +114 -0
  114. package/dist/types/overlays/agent-activity.d.ts +41 -0
  115. package/dist/types/overlays/agent-hub-projection.d.ts +33 -0
  116. package/dist/types/overlays/agent-hub-renderer.d.ts +53 -0
  117. package/dist/types/overlays/agent-hub-types.d.ts +99 -0
  118. package/dist/types/overlays/agent-hub.d.ts +126 -0
  119. package/dist/types/overlays/agent-transcript-viewer.d.ts +71 -0
  120. package/dist/types/overlays/agents-hub.d.ts +72 -0
  121. package/dist/types/overlays/ask-dialog.d.ts +88 -0
  122. package/dist/types/overlays/bordered-loader.d.ts +11 -0
  123. package/dist/types/overlays/btw-history-panel.d.ts +29 -0
  124. package/dist/types/overlays/btw-history.d.ts +17 -0
  125. package/dist/types/overlays/btw-panel.d.ts +26 -0
  126. package/dist/types/overlays/cleanse-panel.d.ts +36 -0
  127. package/dist/types/overlays/codex-reset-fireworks.d.ts +52 -0
  128. package/dist/types/overlays/composer-shape-preview.d.ts +47 -0
  129. package/dist/types/overlays/composer-shape-registry.d.ts +50 -0
  130. package/dist/types/overlays/copy-selector.d.ts +48 -0
  131. package/dist/types/overlays/copy-targets.d.ts +63 -0
  132. package/dist/types/overlays/error-banner.d.ts +13 -0
  133. package/dist/types/overlays/extensions/display-text.d.ts +10 -0
  134. package/dist/types/overlays/extensions/extension-dashboard.d.ts +63 -0
  135. package/dist/types/overlays/extensions/extension-list.d.ts +54 -0
  136. package/dist/types/overlays/extensions/inspector-model.d.ts +109 -0
  137. package/dist/types/overlays/extensions/inspector-panel.d.ts +17 -0
  138. package/dist/types/overlays/extensions/live-tool-session.d.ts +28 -0
  139. package/dist/types/overlays/extensions/mcp-runtime.d.ts +107 -0
  140. package/dist/types/overlays/extensions/state-manager.d.ts +42 -0
  141. package/dist/types/overlays/extensions/types.d.ts +165 -0
  142. package/dist/types/overlays/history-search.d.ts +16 -0
  143. package/dist/types/overlays/hook-editor.d.ts +44 -0
  144. package/dist/types/overlays/hook-input.d.ts +20 -0
  145. package/dist/types/overlays/hook-selector.d.ts +71 -0
  146. package/dist/types/overlays/hub-frame.d.ts +77 -0
  147. package/dist/types/overlays/login-dialog.d.ts +43 -0
  148. package/dist/types/overlays/logout-account-selector.d.ts +15 -0
  149. package/dist/types/overlays/mcp-add-wizard.d.ts +87 -0
  150. package/dist/types/overlays/model-browser.d.ts +213 -0
  151. package/dist/types/overlays/model-hub.d.ts +72 -0
  152. package/dist/types/overlays/model-picker.d.ts +74 -0
  153. package/dist/types/overlays/model-selector.d.ts +52 -0
  154. package/dist/types/overlays/move-overlay.d.ts +39 -0
  155. package/dist/types/overlays/oauth-selector.d.ts +41 -0
  156. package/dist/types/overlays/omfg-panel.d.ts +20 -0
  157. package/dist/types/overlays/pause-screen.d.ts +43 -0
  158. package/dist/types/overlays/plan-review-overlay.d.ts +89 -0
  159. package/dist/types/overlays/plan-save-overlay.d.ts +19 -0
  160. package/dist/types/overlays/plan-toc.d.ts +41 -0
  161. package/dist/types/overlays/plugin-selector.d.ts +28 -0
  162. package/dist/types/overlays/plugin-settings.d.ts +180 -0
  163. package/dist/types/overlays/queue-mode-selector.d.ts +11 -0
  164. package/dist/types/overlays/reset-usage-selector.d.ts +23 -0
  165. package/dist/types/overlays/rewind-selector.d.ts +61 -0
  166. package/dist/types/overlays/running-subagent-badge.d.ts +6 -0
  167. package/dist/types/overlays/session-account-selector.d.ts +24 -0
  168. package/dist/types/overlays/session-info-overlay.d.ts +17 -0
  169. package/dist/types/overlays/session-observer-registry.d.ts +77 -0
  170. package/dist/types/overlays/session-selector.d.ts +131 -0
  171. package/dist/types/overlays/settings-defs.d.ts +119 -0
  172. package/dist/types/overlays/settings-selector.d.ts +73 -0
  173. package/dist/types/overlays/show-images-selector.d.ts +11 -0
  174. package/dist/types/overlays/snapcompact-shape-preview.d.ts +31 -0
  175. package/dist/types/overlays/theme-selector.d.ts +12 -0
  176. package/dist/types/overlays/thinking-selector.d.ts +12 -0
  177. package/dist/types/overlays/tiny-title-download-progress.d.ts +21 -0
  178. package/dist/types/overlays/tree-selector.d.ts +92 -0
  179. package/dist/types/overlays/usage-dashboard.d.ts +94 -0
  180. package/dist/types/overlays/usage-display.d.ts +5 -0
  181. package/dist/types/overlays/usage-row.d.ts +17 -0
  182. package/dist/types/prompt/attachment-chips.d.ts +19 -0
  183. package/dist/types/prompt/composer-attachments.d.ts +71 -0
  184. package/dist/types/prompt/composer-cache.d.ts +37 -0
  185. package/dist/types/prompt/composer.d.ts +159 -0
  186. package/dist/types/prompt/custom-editor.d.ts +261 -0
  187. package/dist/types/prompt/editor-top-gap.d.ts +18 -0
  188. package/dist/types/prompt/emoji-autocomplete.d.ts +16 -0
  189. package/dist/types/prompt/github-ref-autocomplete.d.ts +35 -0
  190. package/dist/types/prompt/gradient-highlight.d.ts +32 -0
  191. package/dist/types/prompt/image-format.d.ts +4 -0
  192. package/dist/types/prompt/image-references.d.ts +26 -0
  193. package/dist/types/prompt/interactive-context-helpers.d.ts +43 -0
  194. package/dist/types/prompt/internal-url-autocomplete.d.ts +56 -0
  195. package/dist/types/prompt/macos-spelling.d.ts +44 -0
  196. package/dist/types/prompt/magic-keyword-boundary.d.ts +9 -0
  197. package/dist/types/prompt/magic-keywords.d.ts +26 -0
  198. package/dist/types/prompt/markdown-prose.d.ts +15 -0
  199. package/dist/types/prompt/model-mention-autocomplete.d.ts +25 -0
  200. package/dist/types/prompt/model-mention-syntax.d.ts +24 -0
  201. package/dist/types/prompt/orchestrate.d.ts +13 -0
  202. package/dist/types/prompt/prompt-action-autocomplete.d.ts +56 -0
  203. package/dist/types/prompt/queue-input.d.ts +8 -0
  204. package/dist/types/prompt/skill-tokens.d.ts +13 -0
  205. package/dist/types/prompt/tools-markdown.d.ts +12 -0
  206. package/dist/types/prompt/ultrathink.d.ts +13 -0
  207. package/dist/types/prompt/usage-amounts.d.ts +26 -0
  208. package/dist/types/prompt/video.d.ts +18 -0
  209. package/dist/types/prompt/welcome.d.ts +83 -0
  210. package/dist/types/prompt/workflow.d.ts +13 -0
  211. package/dist/types/render/code-cell.d.ts +51 -0
  212. package/dist/types/render/file-list.d.ts +22 -0
  213. package/dist/types/render/hyperlink.d.ts +80 -0
  214. package/dist/types/render/index.d.ts +16 -0
  215. package/dist/types/render/output-block.d.ts +54 -0
  216. package/dist/types/render/output-pane.d.ts +80 -0
  217. package/dist/types/render/render-utils.d.ts +312 -0
  218. package/dist/types/render/sixel.d.ts +21 -0
  219. package/dist/types/render/status-line.d.ts +23 -0
  220. package/dist/types/render/tool-card.d.ts +71 -0
  221. package/dist/types/render/tree-list.d.ts +29 -0
  222. package/dist/types/render/types.d.ts +17 -0
  223. package/dist/types/render/utils.d.ts +43 -0
  224. package/dist/types/render/width-aware-text.d.ts +23 -0
  225. package/dist/types/setup/lazy.d.ts +3 -0
  226. package/dist/types/setup/scenes/composer.d.ts +3 -0
  227. package/dist/types/setup/scenes/glyph.d.ts +3 -0
  228. package/dist/types/setup/scenes/model.d.ts +3 -0
  229. package/dist/types/setup/scenes/outro.d.ts +2 -0
  230. package/dist/types/setup/scenes/providers.d.ts +3 -0
  231. package/dist/types/setup/scenes/sign-in.d.ts +21 -0
  232. package/dist/types/setup/scenes/splash.d.ts +11 -0
  233. package/dist/types/setup/scenes/theme.d.ts +3 -0
  234. package/dist/types/setup/scenes/types.d.ts +110 -0
  235. package/dist/types/setup/scenes/web-search.d.ts +22 -0
  236. package/dist/types/setup/setup-version.d.ts +11 -0
  237. package/dist/types/setup/startup-splash.d.ts +9 -0
  238. package/dist/types/setup/wizard-overlay.d.ts +16 -0
  239. package/dist/types/setup/wizard.d.ts +25 -0
  240. package/dist/types/status-line/component.d.ts +182 -0
  241. package/dist/types/status-line/context-usage.d.ts +156 -0
  242. package/dist/types/status-line/footer.d.ts +30 -0
  243. package/dist/types/status-line/git-utils.d.ts +22 -0
  244. package/dist/types/status-line/host.d.ts +129 -0
  245. package/dist/types/status-line/index.d.ts +6 -0
  246. package/dist/types/status-line/loop.d.ts +18 -0
  247. package/dist/types/status-line/metrics.d.ts +21 -0
  248. package/dist/types/status-line/presets.d.ts +3 -0
  249. package/dist/types/status-line/schema.d.ts +15 -0
  250. package/dist/types/status-line/segments.d.ts +5 -0
  251. package/dist/types/status-line/separators.d.ts +3 -0
  252. package/dist/types/status-line/types.d.ts +236 -0
  253. package/dist/types/stdin-buffer.d.ts +4 -0
  254. package/dist/types/theme/color.d.ts +24 -0
  255. package/dist/types/theme/defaults/index.d.ts +9489 -0
  256. package/dist/types/theme/index.d.ts +4 -0
  257. package/dist/types/theme/loader.d.ts +23 -0
  258. package/dist/types/theme/mermaid-cache.d.ts +26 -0
  259. package/dist/types/theme/schema-validation.d.ts +3 -0
  260. package/dist/types/theme/schema.d.ts +25 -0
  261. package/dist/types/theme/session-color.d.ts +43 -0
  262. package/dist/types/theme/shimmer.d.ts +44 -0
  263. package/dist/types/theme/symbols.d.ts +34 -0
  264. package/dist/types/theme/theme-class.d.ts +299 -0
  265. package/dist/types/theme/theme.d.ts +111 -0
  266. package/dist/types/theme/tui-adapters.d.ts +30 -0
  267. package/dist/types/thinking.d.ts +165 -0
  268. package/dist/types/tools/agent-tree.d.ts +36 -0
  269. package/dist/types/tools/ask.d.ts +67 -0
  270. package/dist/types/tools/ast-edit.d.ts +53 -0
  271. package/dist/types/tools/ast-grep.d.ts +52 -0
  272. package/dist/types/tools/autoresearch.d.ts +156 -0
  273. package/dist/types/tools/bash-interactive.d.ts +28 -0
  274. package/dist/types/tools/bash.d.ts +103 -0
  275. package/dist/types/tools/conflict-detect.d.ts +43 -0
  276. package/dist/types/tools/debug.d.ts +60 -0
  277. package/dist/types/tools/default-renderer.d.ts +24 -0
  278. package/dist/types/tools/edit.d.ts +158 -0
  279. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +5 -0
  280. package/dist/types/tools/eval-format/index.d.ts +5 -0
  281. package/dist/types/tools/eval-format/javascript.d.ts +2 -0
  282. package/dist/types/tools/eval-format/python.d.ts +2 -0
  283. package/dist/types/tools/eval.d.ts +94 -0
  284. package/dist/types/tools/fetch.d.ts +42 -0
  285. package/dist/types/tools/gh-format.d.ts +8 -0
  286. package/dist/types/tools/github.d.ts +144 -0
  287. package/dist/types/tools/glob.d.ts +44 -0
  288. package/dist/types/tools/goal.d.ts +42 -0
  289. package/dist/types/tools/grep.d.ts +64 -0
  290. package/dist/types/tools/grouped-file-output.d.ts +71 -0
  291. package/dist/types/tools/hashline-format.d.ts +28 -0
  292. package/dist/types/tools/hub.d.ts +340 -0
  293. package/dist/types/tools/index.d.ts +4 -0
  294. package/dist/types/tools/json-tree.d.ts +59 -0
  295. package/dist/types/tools/line-ranges.d.ts +29 -0
  296. package/dist/types/tools/list-limit.d.ts +15 -0
  297. package/dist/types/tools/lsp.d.ts +72 -0
  298. package/dist/types/tools/match-line-format.d.ts +11 -0
  299. package/dist/types/tools/mcp.d.ts +81 -0
  300. package/dist/types/tools/memory.d.ts +67 -0
  301. package/dist/types/tools/output-meta.d.ts +151 -0
  302. package/dist/types/tools/read.d.ts +139 -0
  303. package/dist/types/tools/renderer.d.ts +98 -0
  304. package/dist/types/tools/report-tool-issue.d.ts +8 -0
  305. package/dist/types/tools/resolve.d.ts +47 -0
  306. package/dist/types/tools/streaming-output.d.ts +282 -0
  307. package/dist/types/tools/subprocess.d.ts +24 -0
  308. package/dist/types/tools/task-repair-args.d.ts +53 -0
  309. package/dist/types/tools/task-yield-assembly.d.ts +20 -0
  310. package/dist/types/tools/task.d.ts +425 -0
  311. package/dist/types/tools/terminal-output.d.ts +8 -0
  312. package/dist/types/tools/think.d.ts +13 -0
  313. package/dist/types/tools/todo.d.ts +118 -0
  314. package/dist/types/tools/tool-errors.d.ts +7 -0
  315. package/dist/types/tools/vibe.d.ts +120 -0
  316. package/dist/types/tools/web-search.d.ts +195 -0
  317. package/dist/types/tools/write.d.ts +76 -0
  318. package/dist/types/tools/xd-url.d.ts +13 -0
  319. package/dist/types/tools/xdev.d.ts +52 -0
  320. package/dist/types/tui.d.ts +18 -0
  321. package/dist/types/utils.d.ts +2 -0
  322. package/package.json +37 -3
  323. package/src/app-keybindings.ts +760 -0
  324. package/src/apps/autoresearch-dashboard.ts +500 -0
  325. package/src/apps/autoresearch-data.ts +64 -0
  326. package/src/apps/cleanse-board.ts +341 -0
  327. package/src/apps/cleanse-picker.ts +57 -0
  328. package/src/apps/debug/log-formatting.ts +53 -0
  329. package/src/apps/debug/log-viewer.ts +868 -0
  330. package/src/apps/debug/protocol-probe.ts +267 -0
  331. package/src/apps/debug/raw-sse-buffer.ts +449 -0
  332. package/src/apps/debug/raw-sse.ts +282 -0
  333. package/src/apps/debug/terminal-info.ts +115 -0
  334. package/src/apps/debug/viewer-frame.ts +174 -0
  335. package/src/apps/git/avatar.ts +52 -0
  336. package/src/apps/git/colors.ts +109 -0
  337. package/src/apps/git/diff-pane.ts +1512 -0
  338. package/src/apps/git/git-tui.ts +889 -0
  339. package/src/apps/git/sidebar.ts +1204 -0
  340. package/src/apps/git/state.ts +113 -0
  341. package/src/apps/if-bench-board.ts +324 -0
  342. package/src/apps/live-visualizer.ts +238 -0
  343. package/src/apps/ps-data.ts +116 -0
  344. package/src/apps/ps-top.ts +422 -0
  345. package/src/apps/session-picker.ts +95 -0
  346. package/src/apps/setup-model-picker.ts +21 -0
  347. package/src/apps/standalone-picker.ts +111 -0
  348. package/src/autocomplete.ts +9 -13
  349. package/src/chat/advisor-message.ts +175 -0
  350. package/src/chat/assistant-message.ts +1214 -0
  351. package/src/chat/background-tan-message.ts +35 -0
  352. package/src/chat/bash-execution.ts +394 -0
  353. package/src/chat/cache-invalidation-marker.ts +94 -0
  354. package/src/chat/chat-transcript-builder.ts +593 -0
  355. package/src/chat/collab-prompt-message.ts +34 -0
  356. package/src/chat/compaction-summary-message.ts +261 -0
  357. package/src/chat/custom-message.ts +23 -0
  358. package/src/chat/display-preferences.ts +24 -0
  359. package/src/chat/eval-execution.ts +172 -0
  360. package/src/chat/execution-shared.ts +113 -0
  361. package/src/chat/extension-types.ts +46 -0
  362. package/src/chat/hook-message.ts +20 -0
  363. package/src/chat/image-loading.ts +122 -0
  364. package/src/chat/late-diagnostics-message.ts +92 -0
  365. package/src/chat/messages.ts +305 -0
  366. package/src/chat/reaction.ts +65 -0
  367. package/src/chat/read-target.ts +7 -0
  368. package/src/chat/read-tool-group.ts +873 -0
  369. package/src/chat/served-model-marker.ts +91 -0
  370. package/src/chat/skill-message.ts +220 -0
  371. package/src/chat/skill-title-input.ts +41 -0
  372. package/src/chat/stripped-tool-calls-placeholder.ts +35 -0
  373. package/src/chat/thinking-display.ts +312 -0
  374. package/src/chat/todo-reminder.ts +33 -0
  375. package/src/chat/tool-execution.ts +1395 -0
  376. package/src/chat/transcript-browser.ts +268 -0
  377. package/src/chat/transcript-entry.ts +96 -0
  378. package/src/chat/transcript-outline.ts +260 -0
  379. package/src/chat/transcript-render-helpers.ts +315 -0
  380. package/src/chat/ttsr-notification.ts +123 -0
  381. package/src/chat/user-message.ts +287 -0
  382. package/src/chrome/chat-block.ts +110 -0
  383. package/src/chrome/collab-qrcode.ts +74 -0
  384. package/src/chrome/context-thresholds.ts +85 -0
  385. package/src/chrome/countdown-timer.ts +74 -0
  386. package/src/chrome/diff.ts +266 -0
  387. package/src/chrome/dynamic-border.ts +36 -0
  388. package/src/chrome/error-block.ts +63 -0
  389. package/src/chrome/form-theme.ts +10 -0
  390. package/src/chrome/format.ts +66 -0
  391. package/src/chrome/index.ts +21 -0
  392. package/src/chrome/keybinding-hints.ts +55 -0
  393. package/src/chrome/live-board.ts +123 -0
  394. package/src/chrome/local-date.ts +20 -0
  395. package/src/chrome/message-divider.ts +51 -0
  396. package/src/chrome/message-frame.ts +164 -0
  397. package/src/chrome/message-notice.ts +103 -0
  398. package/src/chrome/overlay-box.ts +301 -0
  399. package/src/chrome/qrcode.ts +539 -0
  400. package/src/chrome/segment-track.ts +86 -0
  401. package/src/chrome/select-list-mouse-routing.ts +11 -0
  402. package/src/chrome/selector-helpers.ts +119 -0
  403. package/src/chrome/shared.ts +27 -0
  404. package/src/chrome/streaming-panel.ts +72 -0
  405. package/src/chrome/tool-activity.ts +44 -0
  406. package/src/chrome/transcript-container.ts +902 -0
  407. package/src/chrome/transcript-status.ts +27 -0
  408. package/src/chrome/visual-truncate.ts +70 -0
  409. package/src/components/disclosure.ts +235 -0
  410. package/src/components/editor.ts +10 -12
  411. package/src/components/form.ts +566 -0
  412. package/src/components/image.ts +80 -53
  413. package/src/components/input.ts +6 -27
  414. package/src/components/key-value-list.ts +88 -0
  415. package/src/components/layout/geometry.ts +202 -0
  416. package/src/components/layout/row.ts +283 -0
  417. package/src/components/layout/split-pane.ts +349 -0
  418. package/src/components/layout/stack.ts +261 -0
  419. package/src/components/markdown.ts +16 -12
  420. package/src/components/menu-selection.ts +248 -0
  421. package/src/components/metric.ts +132 -0
  422. package/src/components/progress-bar.ts +140 -0
  423. package/src/components/scroll-view.ts +372 -70
  424. package/src/components/scroll-viewport.ts +154 -0
  425. package/src/components/section.ts +77 -0
  426. package/src/components/select-list.ts +238 -159
  427. package/src/components/settings-list.ts +103 -80
  428. package/src/components/table.ts +127 -0
  429. package/src/components/tree-view.ts +481 -0
  430. package/src/components/wizard-step.ts +229 -0
  431. package/src/deccara.ts +1 -3
  432. package/src/hotkeys-markdown.ts +100 -0
  433. package/src/index.ts +18 -0
  434. package/src/keybinding-matchers.ts +99 -0
  435. package/src/keys.ts +2 -2
  436. package/src/lang-from-path.ts +252 -0
  437. package/src/overlays/advisor-config.ts +761 -0
  438. package/src/overlays/agent-activity.ts +113 -0
  439. package/src/overlays/agent-hub-projection.ts +250 -0
  440. package/src/overlays/agent-hub-renderer.ts +281 -0
  441. package/src/overlays/agent-hub-types.ts +74 -0
  442. package/src/overlays/agent-hub.ts +1562 -0
  443. package/src/overlays/agent-transcript-viewer.ts +669 -0
  444. package/src/overlays/agents-hub.ts +1203 -0
  445. package/src/overlays/ask-dialog.ts +1202 -0
  446. package/src/overlays/bordered-loader.ts +41 -0
  447. package/src/overlays/btw-history-panel.ts +591 -0
  448. package/src/overlays/btw-history.ts +33 -0
  449. package/src/overlays/btw-panel.ts +167 -0
  450. package/src/overlays/cleanse-panel.ts +167 -0
  451. package/src/overlays/codex-reset-fireworks.ts +375 -0
  452. package/src/overlays/composer-shape-preview.ts +137 -0
  453. package/src/overlays/composer-shape-registry.ts +82 -0
  454. package/src/overlays/copy-selector.ts +618 -0
  455. package/src/overlays/copy-targets.ts +210 -0
  456. package/src/overlays/error-banner.ts +39 -0
  457. package/src/overlays/extensions/display-text.ts +39 -0
  458. package/src/overlays/extensions/extension-dashboard.ts +645 -0
  459. package/src/overlays/extensions/extension-list.ts +667 -0
  460. package/src/overlays/extensions/inspector-model.ts +484 -0
  461. package/src/overlays/extensions/inspector-panel.ts +669 -0
  462. package/src/overlays/extensions/live-tool-session.ts +67 -0
  463. package/src/overlays/extensions/mcp-runtime.ts +289 -0
  464. package/src/overlays/extensions/state-manager.ts +337 -0
  465. package/src/overlays/extensions/types.ts +208 -0
  466. package/src/overlays/history-search.ts +268 -0
  467. package/src/overlays/hook-editor.ts +272 -0
  468. package/src/overlays/hook-input.ts +84 -0
  469. package/src/overlays/hook-selector.ts +635 -0
  470. package/src/overlays/hub-frame.ts +262 -0
  471. package/src/overlays/login-dialog.ts +231 -0
  472. package/src/overlays/logout-account-selector.ts +115 -0
  473. package/src/overlays/mcp-add-wizard.ts +1436 -0
  474. package/src/overlays/model-browser.ts +1176 -0
  475. package/src/overlays/model-hub.ts +2279 -0
  476. package/src/overlays/model-picker.ts +285 -0
  477. package/src/overlays/model-selector.ts +107 -0
  478. package/src/overlays/move-overlay.ts +197 -0
  479. package/src/overlays/oauth-selector.ts +446 -0
  480. package/src/overlays/omfg-panel.ts +136 -0
  481. package/src/overlays/pause-screen.ts +198 -0
  482. package/src/overlays/plan-review-overlay.ts +1227 -0
  483. package/src/overlays/plan-save-overlay.ts +74 -0
  484. package/src/overlays/plan-toc.ts +139 -0
  485. package/src/overlays/plugin-selector.ts +97 -0
  486. package/src/overlays/plugin-settings.ts +839 -0
  487. package/src/overlays/queue-mode-selector.ts +55 -0
  488. package/src/overlays/reset-usage-selector.ts +170 -0
  489. package/src/overlays/rewind-selector.ts +499 -0
  490. package/src/overlays/running-subagent-badge.ts +19 -0
  491. package/src/overlays/session-account-selector.ts +68 -0
  492. package/src/overlays/session-info-overlay.ts +96 -0
  493. package/src/overlays/session-observer-registry.ts +289 -0
  494. package/src/overlays/session-selector.ts +1137 -0
  495. package/src/overlays/settings-defs.ts +315 -0
  496. package/src/overlays/settings-selector.ts +1415 -0
  497. package/src/overlays/show-images-selector.ts +44 -0
  498. package/src/overlays/snapcompact-shape-preview-doc.md +14 -0
  499. package/src/overlays/snapcompact-shape-preview.ts +192 -0
  500. package/src/overlays/theme-selector.ts +62 -0
  501. package/src/overlays/thinking-selector.ts +51 -0
  502. package/src/overlays/tiny-title-download-progress.ts +102 -0
  503. package/src/overlays/tree-selector.ts +1121 -0
  504. package/src/overlays/usage-dashboard.ts +629 -0
  505. package/src/overlays/usage-display.ts +45 -0
  506. package/src/overlays/usage-row.ts +118 -0
  507. package/src/prompt/attachment-chips.ts +201 -0
  508. package/src/prompt/composer-attachments.ts +280 -0
  509. package/src/prompt/composer-cache.ts +304 -0
  510. package/src/prompt/composer.ts +934 -0
  511. package/src/prompt/custom-editor.ts +1392 -0
  512. package/src/prompt/data/emojis.json +1 -0
  513. package/src/prompt/editor-top-gap.ts +28 -0
  514. package/src/prompt/emoji-autocomplete.ts +285 -0
  515. package/src/prompt/github-ref-autocomplete.ts +75 -0
  516. package/src/prompt/gradient-highlight.ts +101 -0
  517. package/src/prompt/image-format.ts +28 -0
  518. package/src/prompt/image-references.ts +102 -0
  519. package/src/prompt/interactive-context-helpers.ts +126 -0
  520. package/src/prompt/internal-url-autocomplete.ts +155 -0
  521. package/src/prompt/macos-spelling.ts +456 -0
  522. package/src/prompt/magic-keyword-boundary.ts +23 -0
  523. package/src/prompt/magic-keywords.ts +42 -0
  524. package/src/prompt/markdown-prose.ts +245 -0
  525. package/src/prompt/model-mention-autocomplete.ts +94 -0
  526. package/src/prompt/model-mention-syntax.ts +41 -0
  527. package/src/prompt/orchestrate.ts +26 -0
  528. package/src/prompt/prompt-action-autocomplete.ts +337 -0
  529. package/src/prompt/queue-input.ts +132 -0
  530. package/src/prompt/skill-tokens.ts +36 -0
  531. package/src/prompt/tips.txt +27 -0
  532. package/src/prompt/tools-markdown.ts +29 -0
  533. package/src/prompt/ultrathink.ts +26 -0
  534. package/src/prompt/usage-amounts.ts +99 -0
  535. package/src/prompt/video.ts +47 -0
  536. package/src/prompt/welcome.ts +605 -0
  537. package/src/prompt/workflow.ts +26 -0
  538. package/src/render/code-cell.ts +244 -0
  539. package/src/render/file-list.ts +58 -0
  540. package/src/render/hyperlink.ts +192 -0
  541. package/src/render/index.ts +17 -0
  542. package/src/render/output-block.ts +267 -0
  543. package/src/render/output-pane.ts +299 -0
  544. package/src/render/render-utils.ts +1182 -0
  545. package/src/render/sixel.ts +69 -0
  546. package/src/render/status-line.ts +55 -0
  547. package/src/render/tool-card.ts +272 -0
  548. package/src/render/tree-list.ts +188 -0
  549. package/src/render/types.ts +19 -0
  550. package/src/render/utils.ts +109 -0
  551. package/src/render/width-aware-text.ts +60 -0
  552. package/src/setup/lazy.ts +17 -0
  553. package/src/setup/scenes/composer.ts +127 -0
  554. package/src/setup/scenes/glyph.ts +120 -0
  555. package/src/setup/scenes/model.ts +138 -0
  556. package/src/setup/scenes/outro.ts +24 -0
  557. package/src/setup/scenes/providers.ts +107 -0
  558. package/src/setup/scenes/sign-in.ts +373 -0
  559. package/src/setup/scenes/splash.ts +192 -0
  560. package/src/setup/scenes/theme.ts +343 -0
  561. package/src/setup/scenes/types.ts +111 -0
  562. package/src/setup/scenes/web-search.ts +165 -0
  563. package/src/setup/setup-version.ts +11 -0
  564. package/src/setup/startup-splash.ts +107 -0
  565. package/src/setup/wizard-overlay.ts +319 -0
  566. package/src/setup/wizard.ts +101 -0
  567. package/src/status-line/component.ts +3003 -0
  568. package/src/status-line/context-usage.ts +688 -0
  569. package/src/status-line/footer.ts +323 -0
  570. package/src/status-line/git-utils.ts +42 -0
  571. package/src/status-line/host.ts +97 -0
  572. package/src/status-line/index.ts +6 -0
  573. package/src/status-line/loop.ts +28 -0
  574. package/src/status-line/metrics.ts +94 -0
  575. package/src/status-line/presets.ts +107 -0
  576. package/src/status-line/schema.ts +57 -0
  577. package/src/status-line/segments.ts +934 -0
  578. package/src/status-line/separators.ts +55 -0
  579. package/src/status-line/types.ts +227 -0
  580. package/src/stdin-buffer.ts +4 -2
  581. package/src/theme/color.ts +95 -0
  582. package/src/theme/dark.json +95 -0
  583. package/src/theme/defaults/alabaster.json +93 -0
  584. package/src/theme/defaults/amethyst.json +96 -0
  585. package/src/theme/defaults/anthracite.json +93 -0
  586. package/src/theme/defaults/basalt.json +91 -0
  587. package/src/theme/defaults/birch.json +95 -0
  588. package/src/theme/defaults/dark-abyss.json +91 -0
  589. package/src/theme/defaults/dark-arctic.json +104 -0
  590. package/src/theme/defaults/dark-aurora.json +95 -0
  591. package/src/theme/defaults/dark-catppuccin.json +107 -0
  592. package/src/theme/defaults/dark-cavern.json +91 -0
  593. package/src/theme/defaults/dark-celestial.json +106 -0
  594. package/src/theme/defaults/dark-copper.json +95 -0
  595. package/src/theme/defaults/dark-cosmos.json +90 -0
  596. package/src/theme/defaults/dark-cyberpunk.json +102 -0
  597. package/src/theme/defaults/dark-dracula.json +98 -0
  598. package/src/theme/defaults/dark-eclipse.json +91 -0
  599. package/src/theme/defaults/dark-ember.json +95 -0
  600. package/src/theme/defaults/dark-equinox.json +90 -0
  601. package/src/theme/defaults/dark-forest.json +96 -0
  602. package/src/theme/defaults/dark-github.json +105 -0
  603. package/src/theme/defaults/dark-gruvbox.json +112 -0
  604. package/src/theme/defaults/dark-lavender.json +95 -0
  605. package/src/theme/defaults/dark-lunar.json +89 -0
  606. package/src/theme/defaults/dark-midnight.json +95 -0
  607. package/src/theme/defaults/dark-monochrome.json +94 -0
  608. package/src/theme/defaults/dark-monokai.json +98 -0
  609. package/src/theme/defaults/dark-nebula.json +90 -0
  610. package/src/theme/defaults/dark-nord.json +98 -0
  611. package/src/theme/defaults/dark-ocean.json +101 -0
  612. package/src/theme/defaults/dark-one.json +100 -0
  613. package/src/theme/defaults/dark-poimandres.json +115 -0
  614. package/src/theme/defaults/dark-rainforest.json +91 -0
  615. package/src/theme/defaults/dark-reef.json +91 -0
  616. package/src/theme/defaults/dark-retro.json +92 -0
  617. package/src/theme/defaults/dark-rose-pine.json +96 -0
  618. package/src/theme/defaults/dark-sakura.json +95 -0
  619. package/src/theme/defaults/dark-slate.json +95 -0
  620. package/src/theme/defaults/dark-solarized.json +97 -0
  621. package/src/theme/defaults/dark-solstice.json +90 -0
  622. package/src/theme/defaults/dark-starfall.json +91 -0
  623. package/src/theme/defaults/dark-sunset.json +99 -0
  624. package/src/theme/defaults/dark-swamp.json +90 -0
  625. package/src/theme/defaults/dark-synthwave.json +103 -0
  626. package/src/theme/defaults/dark-taiga.json +91 -0
  627. package/src/theme/defaults/dark-terminal.json +95 -0
  628. package/src/theme/defaults/dark-tokyo-night.json +101 -0
  629. package/src/theme/defaults/dark-tundra.json +91 -0
  630. package/src/theme/defaults/dark-twilight.json +91 -0
  631. package/src/theme/defaults/dark-volcanic.json +91 -0
  632. package/src/theme/defaults/graphite.json +92 -0
  633. package/src/theme/defaults/index.ts +201 -0
  634. package/src/theme/defaults/light-arctic.json +107 -0
  635. package/src/theme/defaults/light-aurora-day.json +91 -0
  636. package/src/theme/defaults/light-canyon.json +91 -0
  637. package/src/theme/defaults/light-catppuccin.json +106 -0
  638. package/src/theme/defaults/light-cirrus.json +90 -0
  639. package/src/theme/defaults/light-coral.json +95 -0
  640. package/src/theme/defaults/light-cyberpunk.json +96 -0
  641. package/src/theme/defaults/light-dawn.json +90 -0
  642. package/src/theme/defaults/light-dunes.json +91 -0
  643. package/src/theme/defaults/light-eucalyptus.json +95 -0
  644. package/src/theme/defaults/light-forest.json +100 -0
  645. package/src/theme/defaults/light-frost.json +95 -0
  646. package/src/theme/defaults/light-github.json +115 -0
  647. package/src/theme/defaults/light-glacier.json +91 -0
  648. package/src/theme/defaults/light-gruvbox.json +108 -0
  649. package/src/theme/defaults/light-haze.json +90 -0
  650. package/src/theme/defaults/light-honeycomb.json +95 -0
  651. package/src/theme/defaults/light-lagoon.json +91 -0
  652. package/src/theme/defaults/light-lavender.json +95 -0
  653. package/src/theme/defaults/light-meadow.json +91 -0
  654. package/src/theme/defaults/light-mint.json +95 -0
  655. package/src/theme/defaults/light-monochrome.json +101 -0
  656. package/src/theme/defaults/light-ocean.json +99 -0
  657. package/src/theme/defaults/light-one.json +99 -0
  658. package/src/theme/defaults/light-opal.json +91 -0
  659. package/src/theme/defaults/light-orchard.json +91 -0
  660. package/src/theme/defaults/light-paper.json +95 -0
  661. package/src/theme/defaults/light-poimandres.json +115 -0
  662. package/src/theme/defaults/light-prism.json +90 -0
  663. package/src/theme/defaults/light-retro.json +98 -0
  664. package/src/theme/defaults/light-sand.json +95 -0
  665. package/src/theme/defaults/light-savanna.json +91 -0
  666. package/src/theme/defaults/light-solarized.json +102 -0
  667. package/src/theme/defaults/light-soleil.json +90 -0
  668. package/src/theme/defaults/light-sunset.json +99 -0
  669. package/src/theme/defaults/light-synthwave.json +98 -0
  670. package/src/theme/defaults/light-tokyo-night.json +111 -0
  671. package/src/theme/defaults/light-wetland.json +91 -0
  672. package/src/theme/defaults/light-zenith.json +89 -0
  673. package/src/theme/defaults/limestone.json +94 -0
  674. package/src/theme/defaults/mahogany.json +97 -0
  675. package/src/theme/defaults/marble.json +93 -0
  676. package/src/theme/defaults/obsidian.json +91 -0
  677. package/src/theme/defaults/onyx.json +91 -0
  678. package/src/theme/defaults/pearl.json +93 -0
  679. package/src/theme/defaults/porcelain.json +91 -0
  680. package/src/theme/defaults/quartz.json +96 -0
  681. package/src/theme/defaults/sandstone.json +95 -0
  682. package/src/theme/defaults/titanium.json +90 -0
  683. package/src/theme/index.ts +5 -0
  684. package/src/theme/light.json +93 -0
  685. package/src/theme/loader.ts +200 -0
  686. package/src/theme/mermaid-cache.ts +92 -0
  687. package/src/theme/schema-validation.ts +119 -0
  688. package/src/theme/schema.ts +168 -0
  689. package/src/theme/session-color.ts +253 -0
  690. package/src/theme/shimmer.ts +307 -0
  691. package/src/theme/symbols.ts +1433 -0
  692. package/src/theme/theme-class.ts +787 -0
  693. package/src/theme/theme-schema.json +463 -0
  694. package/src/theme/theme.ts +810 -0
  695. package/src/theme/tui-adapters.ts +339 -0
  696. package/src/thinking.ts +380 -0
  697. package/src/tools/agent-tree.ts +128 -0
  698. package/src/tools/ask.ts +482 -0
  699. package/src/tools/ast-edit.ts +210 -0
  700. package/src/tools/ast-grep.ts +171 -0
  701. package/src/tools/autoresearch.ts +287 -0
  702. package/src/tools/bash-interactive.ts +295 -0
  703. package/src/tools/bash.ts +468 -0
  704. package/src/tools/conflict-detect.ts +77 -0
  705. package/src/tools/debug.ts +167 -0
  706. package/src/tools/default-renderer.ts +172 -0
  707. package/src/tools/edit.ts +1258 -0
  708. package/src/tools/eval-format/code-mode-declarations.ts +61 -0
  709. package/src/tools/eval-format/index.ts +16 -0
  710. package/src/tools/eval-format/javascript.ts +952 -0
  711. package/src/tools/eval-format/python.ts +544 -0
  712. package/src/tools/eval.ts +835 -0
  713. package/src/tools/fetch.ts +226 -0
  714. package/src/tools/gh-format.ts +18 -0
  715. package/src/tools/github.ts +686 -0
  716. package/src/tools/glob.ts +213 -0
  717. package/src/tools/goal.ts +153 -0
  718. package/src/tools/grep.ts +385 -0
  719. package/src/tools/grouped-file-output.ts +211 -0
  720. package/src/tools/hashline-format.ts +59 -0
  721. package/src/tools/hub.ts +1488 -0
  722. package/src/tools/index.ts +71 -0
  723. package/src/tools/json-tree.ts +388 -0
  724. package/src/tools/line-ranges.ts +98 -0
  725. package/src/tools/list-limit.ts +43 -0
  726. package/src/tools/lsp.ts +702 -0
  727. package/src/tools/match-line-format.ts +20 -0
  728. package/src/tools/mcp.ts +309 -0
  729. package/src/tools/memory.ts +220 -0
  730. package/src/tools/output-meta.ts +367 -0
  731. package/src/tools/read.ts +628 -0
  732. package/src/tools/renderer.ts +107 -0
  733. package/src/tools/report-tool-issue.ts +13 -0
  734. package/src/tools/resolve.ts +139 -0
  735. package/src/tools/streaming-output.ts +1576 -0
  736. package/src/tools/subprocess.ts +39 -0
  737. package/src/tools/task-repair-args.ts +118 -0
  738. package/src/tools/task-yield-assembly.ts +147 -0
  739. package/src/tools/task.ts +2110 -0
  740. package/src/tools/terminal-output.ts +153 -0
  741. package/src/tools/think.ts +28 -0
  742. package/src/tools/todo.ts +580 -0
  743. package/src/tools/tool-errors.ts +15 -0
  744. package/src/tools/vibe.ts +420 -0
  745. package/src/tools/web-search.ts +409 -0
  746. package/src/tools/write.ts +511 -0
  747. package/src/tools/xd-url.ts +28 -0
  748. package/src/tools/xdev.ts +201 -0
  749. package/src/tui.ts +70 -8
  750. package/src/utils.ts +8 -0
@@ -0,0 +1,2110 @@
1
+ import type { Usage } from "@oh-my-pi/pi-ai";
2
+ import { isRecord } from "@oh-my-pi/pi-utils";
3
+ import type { ThemeColor } from "../theme/theme";
4
+ import type { ConfiguredThinkingLevel } from "../render/render-utils";
5
+ import type { ToolRenderer } from "./renderer";
6
+ /**
7
+ * TUI rendering for task tool.
8
+ *
9
+ * Provides renderCall and renderResult functions for displaying
10
+ * task execution in the terminal UI.
11
+ */
12
+ import * as path from "node:path";
13
+ import { Container, type Component } from "../tui";
14
+ import { Markdown } from "../components/markdown";
15
+ import { Text } from "../components/text";
16
+ import { visibleWidth, wrapTextWithAnsi } from "../utils";
17
+ import { sanitizeText } from "@oh-my-pi/pi-utils";
18
+ import type { RenderResultOptions } from "./renderer";
19
+ import { formatAgentStatRun, renderAgentTreeRow } from "./agent-tree";
20
+ import { getMarkdownTheme, type Theme } from "../theme/theme";
21
+ import { stripGeneratedOutputNotice, stripRawOutputArtifactNotice, stripTrailingNotice } from "./output-meta";
22
+ import {
23
+ capPreviewLines,
24
+ FEED_MODEL_BADGE_WIDTH,
25
+ formatBadge,
26
+ formatDuration,
27
+ formatExpandHint,
28
+ formatFeedModelBadge,
29
+ formatMoreItems,
30
+ formatNumber,
31
+ isFeedModelBadgeEnabled,
32
+ previewLine,
33
+ previewWindowRows,
34
+ replaceTabs,
35
+ shortenPath,
36
+ type ToolUIStatus,
37
+ TRUNCATE_LENGTHS,
38
+ truncateToWidth,
39
+ } from "../render/render-utils";
40
+ import { renderStatusLine } from "../render/index";
41
+ import { framedToolCard } from "../render/tool-card";
42
+ import { formatOutputInline, renderJsonTreeLines } from "./json-tree";
43
+ import { repairDoubleEncodedJsonString } from "./task-repair-args";
44
+ import { getSubprocessToolRenderer } from "./subprocess";
45
+ import { assembleYieldResult } from "./task-yield-assembly";
46
+
47
+ /** Render context threaded in from `ToolExecutionComponent.#buildRenderContext`. */
48
+ interface TaskRenderContext {
49
+ hasResult?: boolean;
50
+ /**
51
+ * The block left the transcript live region (detached spawn the transcript
52
+ * has moved past, or a sealed block): progress rows render static gray, so
53
+ * commit-eligible rows do not repaint after entering native scrollback.
54
+ */
55
+ frozen?: boolean;
56
+ /**
57
+ * Wall clock for time-derived rows (current-tool elapsed, retry countdown).
58
+ * The component freezes it once the block settles or any of its rows enter
59
+ * native scrollback, so identical-input rebuilds stay byte-identical with
60
+ * committed history. Absent: render with the live clock.
61
+ */
62
+ nowMs?: number;
63
+ }
64
+ type TaskRenderOptions = RenderResultOptions & { renderContext?: TaskRenderContext };
65
+
66
+ const MAX_NESTED_TASK_RENDER_DEPTH = 8;
67
+
68
+ function renderNestedCycleLine(theme: Theme): string {
69
+ return theme.fg("dim", "… nested task progress already shown");
70
+ }
71
+
72
+ function formatFindingSummary(findings: FindingDetails[], theme: Theme): string {
73
+ if (findings.length === 0) return theme.fg("dim", "Findings: none");
74
+
75
+ const counts: { [P in FindingPriority]?: number } = {};
76
+ for (const finding of findings) {
77
+ counts[finding.priority] = (counts[finding.priority] ?? 0) + 1;
78
+ }
79
+
80
+ const parts: string[] = [];
81
+ for (const label of PRIORITY_LABELS) {
82
+ const { symbol, color } = getPriorityInfo(label);
83
+ const count = counts[label] ?? 0;
84
+ const text = theme.fg(color, `${label}:${count}`);
85
+ parts.push(theme.styledSymbol(symbol, color) ? `${theme.styledSymbol(symbol, color)} ${text}` : text);
86
+ }
87
+
88
+ return `${theme.fg("dim", "Findings:")} ${parts.join(theme.sep.dot)}`;
89
+ }
90
+
91
+ function normalizeFindings(value: unknown): FindingDetails[] {
92
+ if (!Array.isArray(value)) return [];
93
+ const findings: FindingDetails[] = [];
94
+ for (const item of value) {
95
+ const finding = parseFindingDetails(item);
96
+ if (finding) findings.push(finding);
97
+ }
98
+ return findings;
99
+ }
100
+
101
+ /** Reviewer output declares `findings` as an array, so a lone finding section still assembles as a list. */
102
+ const REVIEWER_ARRAY_LABELS: ReadonlySet<string> = new Set(["findings"]);
103
+
104
+ function extractIncrementalReviewResult(
105
+ items: RenderYieldItem[],
106
+ ): { summary: SubmitReviewDetails; findings: FindingDetails[] } | undefined {
107
+ const yieldItems: YieldItem[] = items.map(item => ({
108
+ data: item.data,
109
+ type: item.type,
110
+ status: item.status === "aborted" ? "aborted" : item.status === "success" ? "success" : undefined,
111
+ useLastTurn: item.useLastTurn,
112
+ }));
113
+ const assembled = assembleYieldResult(yieldItems, undefined, REVIEWER_ARRAY_LABELS);
114
+ const data = assembled?.data;
115
+ if (!data || typeof data !== "object" || Array.isArray(data)) return undefined;
116
+ const record = data as Record<string, unknown>;
117
+ const overallCorrectness = record.overall_correctness;
118
+ const explanation = record.explanation;
119
+ const confidence = record.confidence;
120
+ if (
121
+ (overallCorrectness !== "correct" && overallCorrectness !== "incorrect") ||
122
+ typeof explanation !== "string" ||
123
+ typeof confidence !== "number"
124
+ ) {
125
+ return undefined;
126
+ }
127
+ return {
128
+ summary: {
129
+ overall_correctness: overallCorrectness,
130
+ explanation,
131
+ confidence,
132
+ },
133
+ findings: normalizeFindings(record.findings),
134
+ };
135
+ }
136
+
137
+ interface RenderYieldItem {
138
+ data?: unknown;
139
+ type?: string | string[];
140
+ status?: string;
141
+ useLastTurn?: boolean;
142
+ }
143
+
144
+ /**
145
+ * Normalize the `yield` slot of `extractedToolData` into an array of
146
+ * yield-detail records. The subprocess executor always populates this slot as
147
+ * `unknown[]` (see `executor.ts` `extractData` handler), but the renderer
148
+ * MUST also tolerate a stray single object — optional chaining short-circuits
149
+ * on `null`/`undefined` only, so calling `.map` on a plain object would throw
150
+ * `TypeError: completeData?.map is not a function` and crash the TUI.
151
+ * A single object is wrapped as a 1-element array so the review verdict still
152
+ * renders; non-object primitives drop out.
153
+ */
154
+ function normalizeYieldData(value: unknown): RenderYieldItem[] {
155
+ const items = Array.isArray(value) ? value : value !== null && typeof value === "object" ? [value] : [];
156
+ const normalized: RenderYieldItem[] = [];
157
+ for (const item of items) {
158
+ if (item === null || typeof item !== "object") continue;
159
+ const record = item as Record<string, unknown>;
160
+ const typeValue = record.type;
161
+ let type: RenderYieldItem["type"];
162
+ if (typeof typeValue === "string") {
163
+ type = typeValue;
164
+ } else if (Array.isArray(typeValue)) {
165
+ const labels: string[] = [];
166
+ let allLabels = true;
167
+ for (const label of typeValue) {
168
+ if (typeof label !== "string") {
169
+ allLabels = false;
170
+ break;
171
+ }
172
+ labels.push(label);
173
+ }
174
+ if (allLabels) type = labels;
175
+ }
176
+ normalized.push({
177
+ data: record.data,
178
+ type,
179
+ status: typeof record.status === "string" ? record.status : undefined,
180
+ useLastTurn: record.useLastTurn === true ? true : undefined,
181
+ });
182
+ }
183
+ return normalized;
184
+ }
185
+
186
+ function getRenderYieldLabels(type: RenderYieldItem["type"]): string[] {
187
+ if (typeof type === "string") {
188
+ const label = type.trim();
189
+ return label ? [label] : [];
190
+ }
191
+ if (!Array.isArray(type)) return [];
192
+ const labels: string[] = [];
193
+ for (const value of type) {
194
+ const label = value.trim();
195
+ if (label) labels.push(label);
196
+ }
197
+ return labels;
198
+ }
199
+
200
+ function formatYieldPreview(item: RenderYieldItem): string {
201
+ if (item.useLastTurn === true && item.data === undefined) return "last assistant turn";
202
+ if (item.data === undefined) return "last assistant turn";
203
+ if (typeof item.data === "string") return previewLine(sanitizeText(item.data), 70);
204
+ try {
205
+ return previewLine(sanitizeText(JSON.stringify(item.data) ?? "null"), 70);
206
+ } catch {
207
+ return previewLine(sanitizeText(String(item.data)), 70);
208
+ }
209
+ }
210
+
211
+ function renderTypedYieldSections(value: unknown, continuePrefix: string, expanded: boolean, theme: Theme): string[] {
212
+ const typedItems: Array<{ item: RenderYieldItem; labels: string[] }> = [];
213
+ for (const item of normalizeYieldData(value)) {
214
+ const labels = getRenderYieldLabels(item.type);
215
+ if (labels.length === 0) continue;
216
+ typedItems.push({ item, labels });
217
+ }
218
+ const displayCount = expanded ? typedItems.length : 3;
219
+ const lines: string[] = [];
220
+ for (const { item, labels } of typedItems.slice(-displayCount)) {
221
+ const terminal = !Array.isArray(item.type);
222
+ const prefix = terminal ? "yield" : "yield+";
223
+ const label = `${prefix}[${labels.join(", ")}]`;
224
+ lines.push(`${continuePrefix}${theme.fg("dim", label)}: ${theme.fg("dim", formatYieldPreview(item))}`);
225
+ }
226
+ if (typedItems.length > displayCount) {
227
+ lines.push(`${continuePrefix}${theme.fg("dim", formatMoreItems(typedItems.length - displayCount, "yield"))}`);
228
+ }
229
+ return lines;
230
+ }
231
+
232
+ /** Formats sanitized task identifiers as hierarchy breadcrumbs. */
233
+ export function formatTaskId(id: string): string {
234
+ // Ids are name-based (e.g. "Anna", "Anna-2"); a "." separates nesting levels
235
+ // (e.g. "Anna.Bob"). Render the hierarchy with a ">" breadcrumb.
236
+ const sanitizedId = sanitizeText(id);
237
+ const segments = sanitizedId.split(".");
238
+ return segments.length < 2 ? sanitizedId : segments.join(">");
239
+ }
240
+
241
+ const MISSING_YIELD_WARNING_PREFIX = "SYSTEM WARNING: Subagent exited without calling yield tool";
242
+
243
+ function extractMissingYieldWarning(output: string): { warning?: string; rest: string } {
244
+ const lines = output.split("\n");
245
+ const firstLine = lines[0]?.trim() ?? "";
246
+ if (!firstLine.startsWith(MISSING_YIELD_WARNING_PREFIX)) {
247
+ return { rest: output };
248
+ }
249
+ const rest = lines
250
+ .slice(1)
251
+ .join("\n")
252
+ .replace(/^\s*\n+/, "");
253
+ return { warning: firstLine, rest };
254
+ }
255
+
256
+ const BASH_WALL_TIME_NOTICE_RE = /^Wall time: \d+(?:\.\d+)? seconds$/u;
257
+ const BASH_EXIT_CODE_NOTICE_RE = /^Command exited with code -?\d+$/u;
258
+
259
+ function sanitizeRecentOutput(output: string): string {
260
+ let text = sanitizeText(output).trimEnd();
261
+ while (text) {
262
+ const withoutArtifactNotice = stripRawOutputArtifactNotice(text).text;
263
+ if (withoutArtifactNotice !== text) {
264
+ text = withoutArtifactNotice;
265
+ continue;
266
+ }
267
+ const withoutOutputNotice = stripGeneratedOutputNotice(text);
268
+ if (withoutOutputNotice !== text) {
269
+ text = withoutOutputNotice;
270
+ continue;
271
+ }
272
+ const withoutRuntimeNotice = stripTrailingNotice(
273
+ text,
274
+ line => BASH_WALL_TIME_NOTICE_RE.test(line) || BASH_EXIT_CODE_NOTICE_RE.test(line),
275
+ );
276
+ if (withoutRuntimeNotice !== text) {
277
+ text = withoutRuntimeNotice;
278
+ continue;
279
+ }
280
+ break;
281
+ }
282
+ return text;
283
+ }
284
+
285
+ function renderOutputSection(
286
+ output: string,
287
+ continuePrefix: string,
288
+ expanded: boolean,
289
+ theme: Theme,
290
+ maxCollapsed = 3,
291
+ maxExpanded = 10,
292
+ warning?: string,
293
+ ): string[] {
294
+ const lines: string[] = [];
295
+ const sanitizedOutput = sanitizeText(output);
296
+ const trimmedOutput = sanitizedOutput.trimEnd();
297
+ if (!trimmedOutput && !warning) return lines;
298
+
299
+ if (warning) {
300
+ lines.push(`${continuePrefix}${theme.fg("dim", "Output")}`);
301
+ lines.push(
302
+ `${continuePrefix} ${theme.fg("warning", theme.status.warning)} ${theme.fg(
303
+ "dim",
304
+ truncateToWidth(sanitizeText(warning), 80),
305
+ )}`,
306
+ );
307
+
308
+ if (!trimmedOutput) {
309
+ return lines;
310
+ }
311
+
312
+ if (trimmedOutput.startsWith("{") || trimmedOutput.startsWith("[")) {
313
+ try {
314
+ const parsed = JSON.parse(trimmedOutput);
315
+
316
+ if (!expanded) {
317
+ lines.push(`${continuePrefix} ${theme.fg("dim", formatOutputInline(parsed))}`);
318
+ return lines;
319
+ }
320
+
321
+ const tree = renderJsonTreeLines(parsed, theme, {
322
+ maxDepth: expanded ? 6 : 2,
323
+ maxLines: expanded ? 24 : 6,
324
+ maxScalarLen: 70,
325
+ sanitizeText,
326
+ hiddenRootKeys: [],
327
+ multilineStrings: true,
328
+ rootConnectors: "siblings",
329
+ escapeStringWhitespace: false,
330
+ });
331
+ if (tree.lines.length > 0) {
332
+ for (const line of tree.lines) {
333
+ lines.push(`${continuePrefix} ${line}`);
334
+ }
335
+ if (tree.truncated) {
336
+ lines.push(`${continuePrefix} ${theme.fg("dim", "…")}`);
337
+ }
338
+ return lines;
339
+ }
340
+ } catch {
341
+ // Fall back to raw output
342
+ }
343
+ }
344
+
345
+ const outputLines = trimmedOutput.split("\n");
346
+ const previewCount = expanded ? maxExpanded : maxCollapsed;
347
+ for (const line of outputLines.slice(0, previewCount)) {
348
+ lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(replaceTabs(line), 70))}`);
349
+ }
350
+
351
+ if (outputLines.length > previewCount) {
352
+ lines.push(
353
+ `${continuePrefix} ${theme.fg("dim", formatMoreItems(outputLines.length - previewCount, "line"))}`,
354
+ );
355
+ }
356
+
357
+ return lines;
358
+ }
359
+
360
+ if (trimmedOutput.startsWith("{") || trimmedOutput.startsWith("[")) {
361
+ try {
362
+ const parsed = JSON.parse(trimmedOutput);
363
+
364
+ // Collapsed: inline format like Args
365
+ if (!expanded) {
366
+ lines.push(`${continuePrefix}${theme.fg("dim", formatOutputInline(parsed))}`);
367
+ return lines;
368
+ }
369
+
370
+ // Expanded: tree format
371
+ lines.push(`${continuePrefix}${theme.fg("dim", "Output")}`);
372
+ const tree = renderJsonTreeLines(parsed, theme, {
373
+ maxDepth: expanded ? 6 : 2,
374
+ maxLines: expanded ? 24 : 6,
375
+ maxScalarLen: 70,
376
+ sanitizeText,
377
+ hiddenRootKeys: [],
378
+ multilineStrings: true,
379
+ rootConnectors: "siblings",
380
+ escapeStringWhitespace: false,
381
+ });
382
+ if (tree.lines.length > 0) {
383
+ for (const line of tree.lines) {
384
+ lines.push(`${continuePrefix} ${line}`);
385
+ }
386
+ if (tree.truncated) {
387
+ lines.push(`${continuePrefix} ${theme.fg("dim", "…")}`);
388
+ }
389
+ return lines;
390
+ }
391
+ } catch {
392
+ // Fall back to raw output
393
+ }
394
+ }
395
+
396
+ lines.push(`${continuePrefix}${theme.fg("dim", "Output")}`);
397
+
398
+ const outputLines = trimmedOutput.split("\n");
399
+ const previewCount = expanded ? maxExpanded : maxCollapsed;
400
+ for (const line of outputLines.slice(0, previewCount)) {
401
+ lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(replaceTabs(line), 70))}`);
402
+ }
403
+
404
+ if (outputLines.length > previewCount) {
405
+ lines.push(`${continuePrefix} ${theme.fg("dim", formatMoreItems(outputLines.length - previewCount, "line"))}`);
406
+ }
407
+
408
+ return lines;
409
+ }
410
+
411
+ function renderTaskSection(
412
+ task: string,
413
+ continuePrefix: string,
414
+ expanded: boolean,
415
+ theme: Theme,
416
+ maxExpanded = 20,
417
+ ): string[] {
418
+ const lines: string[] = [];
419
+ const trimmed = sanitizeText(task).trim();
420
+ if (!expanded || !trimmed) return lines;
421
+
422
+ lines.push(`${continuePrefix}${theme.fg("dim", "Task")}`);
423
+ const taskLines = trimmed.split("\n");
424
+ for (const line of taskLines.slice(0, maxExpanded)) {
425
+ lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(replaceTabs(line), 70))}`);
426
+ }
427
+ if (taskLines.length > maxExpanded) {
428
+ lines.push(`${continuePrefix} ${theme.fg("dim", formatMoreItems(taskLines.length - maxExpanded, "line"))}`);
429
+ }
430
+
431
+ return lines;
432
+ }
433
+
434
+ /**
435
+ * First line of a streamed `task` brief, trimmed — a row's secondary text.
436
+ * The args stream in token by token, so non-string values fall through to "".
437
+ */
438
+ function taskFirstLine(task: unknown): string {
439
+ if (typeof task !== "string") return "";
440
+ const trimmed = sanitizeText(task).trim();
441
+ const newline = trimmed.indexOf("\n");
442
+ return newline === -1 ? trimmed : trimmed.slice(0, newline);
443
+ }
444
+
445
+ /**
446
+ * Header label for a task call while nothing has spawned yet: the flat form's
447
+ * `agent` type. Batch calls return undefined — each item row carries its own
448
+ * `⟨agent⟩` badge, so a joined list in the header would just repeat them.
449
+ */
450
+ function formatAgentHeaderLabel(args: Partial<TaskParams> | undefined): string | undefined {
451
+ if (!args) return undefined;
452
+ const flat = typeof args.agent === "string" ? args.agent.trim() : "";
453
+ return flat || undefined;
454
+ }
455
+
456
+ /** Dim `⟨agent⟩` badge for a non-default agent type; empty for the generic worker. */
457
+ export function agentTypeBadge(agent: string | undefined, theme: Theme): string {
458
+ const trimmed = agent?.trim();
459
+ if (!trimmed || trimmed === "task") return "";
460
+ return ` ${theme.fg("dim", `${theme.format.bracketLeft}${trimmed}${theme.format.bracketRight}`)}`;
461
+ }
462
+
463
+ /**
464
+ * Render the call preview lines for the single spawned agent. The
465
+ * args stream in token by token, so every field access is defensive.
466
+ */
467
+ function renderTaskCallLines(args: Partial<TaskParams> | undefined, theme: Theme): string[] {
468
+ if (!args) return [];
469
+ const bullet = theme.fg("dim", "•");
470
+ const lines: string[] = [];
471
+
472
+ const rawName = typeof args.name === "string" ? args.name.trim() : "";
473
+ const idLabel = rawName ? formatTaskId(rawName) : "";
474
+ const brief = taskFirstLine(args.task);
475
+ if (idLabel || brief) {
476
+ let line = `${bullet} ${theme.fg("accent", theme.bold(idLabel || "agent"))}`;
477
+ if (brief) {
478
+ line += `: ${theme.fg("muted", previewLine(brief, 64))}`;
479
+ }
480
+ line += agentTypeBadge(args.agent, theme);
481
+ lines.push(line);
482
+ }
483
+ lines.push(...renderTaskItemLines(args.tasks, theme));
484
+ return lines;
485
+ }
486
+
487
+ /**
488
+ * Agent rows shown per collapsed task list; the rest fold into a single
489
+ * `… N more agents` summary line (expand uncaps).
490
+ */
491
+ const COLLAPSED_AGENT_LIMIT = 4;
492
+
493
+ /**
494
+ * Render the per-item list (`name` + `task` brief) for a batch call's
495
+ * streaming preview. The args stream in token by token, so the array grows
496
+ * over time and trailing entries may be partially parsed — every field access
497
+ * is defensive.
498
+ */
499
+ function renderTaskItemLines(tasks: TaskItem[] | undefined, theme: Theme): string[] {
500
+ if (!Array.isArray(tasks) || tasks.length === 0) return [];
501
+
502
+ const bullet = theme.fg("dim", "•");
503
+ const cap = Math.min(tasks.length, COLLAPSED_AGENT_LIMIT);
504
+ const lines: string[] = [];
505
+ for (let i = 0; i < cap; i++) {
506
+ const item = tasks[i] as Partial<TaskItem> | undefined;
507
+ const rawName = typeof item?.name === "string" ? item.name.trim() : "";
508
+ const idLabel = rawName ? formatTaskId(rawName) : `#${i + 1}`;
509
+ let line = `${bullet} ${theme.fg("accent", theme.bold(idLabel))}`;
510
+ const brief = taskFirstLine(item?.task);
511
+ if (brief) {
512
+ line += `: ${theme.fg("muted", previewLine(brief, 64))}`;
513
+ }
514
+ line += agentTypeBadge(item?.agent, theme);
515
+ if (item?.isolated === true) {
516
+ line += theme.fg("dim", " [isolated]");
517
+ }
518
+ lines.push(line);
519
+ }
520
+ if (cap < tasks.length) {
521
+ lines.push(`${bullet} ${theme.fg("dim", formatMoreItems(tasks.length - cap, "agent"))}`);
522
+ }
523
+ return lines;
524
+ }
525
+
526
+ /** One renderable frame section: optional label, body rows, leading divider. */
527
+ type TaskRenderSection = { label?: string; content: readonly string[]; separator?: boolean };
528
+ type AssignmentSectionRenderer = (width: number) => TaskRenderSection;
529
+
530
+ // Default output-block layout is: left border + one-cell content inset + right
531
+ // border. Render markdown at that inner width so the output block does not need
532
+ // to rewrap already-rendered assignment lines.
533
+ const ASSIGNMENT_FRAME_INSET = 3;
534
+
535
+ /**
536
+ * Build the assignment section (the markdown brief handed to the subagent).
537
+ * Rendered in both the streaming call preview and the result frame so the
538
+ * brief stays visible for the whole task lifecycle — not just until the first
539
+ * progress snapshot replaces the call view.
540
+ */
541
+ function createAssignmentSectionRenderer(
542
+ args: Partial<TaskParams> | undefined,
543
+ theme: Theme,
544
+ ): AssignmentSectionRenderer | undefined {
545
+ // `renderResult` receives the raw tool args (unlike `renderCall`, which is
546
+ // fed through `repairTaskParams`), so undo any per-field double-encoding
547
+ // here too. The repair is idempotent on already-clean text.
548
+ const assignment = sanitizeText(
549
+ repairDoubleEncodedJsonString(typeof args?.task === "string" ? args.task : ""),
550
+ ).trim();
551
+ if (!assignment) return undefined;
552
+ return createMarkdownSectionRenderer(assignment, theme);
553
+ }
554
+
555
+ /**
556
+ * Build the shared-context section (the `# Goal / # Constraints` background a
557
+ * batch call hands every subagent). Rendered like the assignment brief so the
558
+ * shared background stays visible for the whole task lifecycle.
559
+ */
560
+ function createContextSectionRenderer(
561
+ args: Partial<TaskParams> | undefined,
562
+ theme: Theme,
563
+ ): AssignmentSectionRenderer | undefined {
564
+ const context = sanitizeText(
565
+ repairDoubleEncodedJsonString(typeof args?.context === "string" ? args.context : ""),
566
+ ).trim();
567
+ if (!context) return undefined;
568
+ return createMarkdownSectionRenderer(context, theme);
569
+ }
570
+
571
+ function createMarkdownSectionRenderer(text: string, theme: Theme): AssignmentSectionRenderer {
572
+ const markdown = new Markdown(text, 0, 0, getMarkdownTheme(), {
573
+ color: line => theme.fg("muted", line),
574
+ });
575
+ return width => ({ content: markdown.render(Math.max(1, width - ASSIGNMENT_FRAME_INSET)) });
576
+ }
577
+
578
+ /**
579
+ * Render the tool call arguments.
580
+ */
581
+ export function renderCall(args: TaskParams, options: TaskRenderOptions, theme: Theme): Component {
582
+ const showIsolated = "isolated" in args && args.isolated === true;
583
+ // Dispatch glyph from the first frame: spawning is non-blocking, so a
584
+ // pending/hourglass icon would misread the call as something the turn
585
+ // waits on.
586
+ const header = renderStatusLine(
587
+ {
588
+ iconOverride: theme.styledSymbol("tool.task", "accent"),
589
+ title: "Task",
590
+ description: formatAgentHeaderLabel(args),
591
+ },
592
+ theme,
593
+ );
594
+ const assignmentSection = createAssignmentSectionRenderer(args, theme);
595
+ const contextSection = createContextSectionRenderer(args, theme);
596
+ return framedToolCard(theme, ({ width }) => {
597
+ const sections: TaskRenderSection[] = [];
598
+
599
+ // The call preview only exists to surface the dispatched agent while the
600
+ // args stream in. Once a result snapshot exists, `renderResult` draws the
601
+ // same agent (and the assignment brief) itself, so showing it here would
602
+ // repeat what the result frame already shows.
603
+ if (!options.renderContext?.hasResult) {
604
+ // Mirror renderResult's layout — context, assignment, then the
605
+ // per-agent list — so the agent rows do not jump from above the
606
+ // brief to below it when the first progress snapshot replaces the
607
+ // call view. This also matches the schema's field order (`context`
608
+ // streams before `tasks`), so the streaming preview grows
609
+ // append-only instead of inserting agent rows above the
610
+ // already-rendered markdown and pushing it down on every item.
611
+ if (contextSection) sections.push(contextSection(width));
612
+ if (assignmentSection) sections.push(assignmentSection(width));
613
+ const callLines = renderTaskCallLines(args, theme);
614
+ // Guarded: an empty trailing section would still draw its divider.
615
+ if (callLines.length > 0) sections.push({ separator: true, content: callLines });
616
+ }
617
+
618
+ return {
619
+ header,
620
+ headerMeta: showIsolated ? "isolated" : undefined,
621
+ sections,
622
+ phase: "pending",
623
+ borderColor: "borderMuted",
624
+ };
625
+ });
626
+ }
627
+
628
+ function truncateTaskRow(text: string, width: number, ellipsis?: ""): string {
629
+ return Number.isFinite(width) ? truncateToWidth(text, width, ellipsis) : text;
630
+ }
631
+
632
+ function renderDescriptionLines(description: string, prefix: string, width: number, theme: Theme): string[] {
633
+ if (!Number.isFinite(width)) return description.split("\n").map(line => `${prefix}${theme.fg("dim", line)}`);
634
+ const boundedPrefix = truncateToWidth(prefix, Math.max(0, width - 1), "");
635
+ const contentWidth = Math.max(1, width - visibleWidth(boundedPrefix));
636
+ return wrapTextWithAnsi(description, contentWidth).map(line => `${boundedPrefix}${theme.fg("dim", line)}`);
637
+ }
638
+
639
+ /**
640
+ * Render streaming progress for a single agent.
641
+ */
642
+ function renderAgentProgress(
643
+ progress: AgentProgress,
644
+ prefix: string,
645
+ continuePrefix: string,
646
+ expanded: boolean,
647
+ theme: Theme,
648
+ spinnerFrame?: number,
649
+ frozen = false,
650
+ seenNestedTasks?: WeakSet<object>,
651
+ nestedDepth = 0,
652
+ nowMs = Date.now(),
653
+ maxWidth = Number.POSITIVE_INFINITY,
654
+ ): string[] {
655
+ const lines: string[] = [];
656
+
657
+ const fullDescription = progress.description ? replaceTabs(sanitizeText(progress.description)).trim() : undefined;
658
+ let statusBadge: string | undefined;
659
+ if (progress.retryState && progress.status === "running") {
660
+ statusBadge = ` ${formatBadge("retrying", "warning", theme)}`;
661
+ } else if (progress.retryFailure && (progress.status === "failed" || progress.status === "aborted")) {
662
+ statusBadge = ` ${formatBadge("rate-limited", "error", theme)}`;
663
+ }
664
+ const row = renderAgentTreeRow(
665
+ {
666
+ presentation: "task",
667
+ status: progress.status,
668
+ prefix,
669
+ id: formatTaskId(progress.id),
670
+ width: maxWidth,
671
+ model: progress.resolvedModelIdentity ?? progress.resolvedModel,
672
+ thinkingLevel: progress.resolvedThinkingLevel,
673
+ advisor: progress.advisor,
674
+ spinnerFrame,
675
+ frozen,
676
+ roleBadge: agentTypeBadge(progress.agent, theme),
677
+ statusBadge,
678
+ description: fullDescription,
679
+ preview:
680
+ progress.status === "running" && !fullDescription
681
+ ? ` ${theme.fg("muted", previewLine(sanitizeText(progress.assignment ?? progress.task), 40))}`
682
+ : undefined,
683
+ stats: progress.status === "running" || progress.status === "completed" ? progress : undefined,
684
+ },
685
+ theme,
686
+ );
687
+ lines.push(row.line);
688
+ if (fullDescription && !row.descriptionShown) {
689
+ lines.push(...renderDescriptionLines(fullDescription, continuePrefix, maxWidth, theme));
690
+ }
691
+
692
+ lines.push(...renderTaskSection(progress.assignment ?? progress.task, continuePrefix, expanded, theme));
693
+
694
+ // Current tool (if running) or most recent completed tool
695
+ if (progress.status === "running") {
696
+ if (progress.currentTool) {
697
+ let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", sanitizeText(progress.currentTool))}`;
698
+ const toolDetail = progress.lastIntent ?? progress.currentToolArgs;
699
+ if (toolDetail) {
700
+ toolLine += `: ${theme.fg("dim", previewLine(sanitizeText(toolDetail), 40))}`;
701
+ }
702
+ if (progress.currentToolStartMs) {
703
+ const elapsed = nowMs - progress.currentToolStartMs;
704
+ if (elapsed > 5000) {
705
+ toolLine += `${theme.sep.dot}${theme.fg("warning", formatDuration(elapsed))}`;
706
+ }
707
+ }
708
+ lines.push(toolLine);
709
+ } else if (progress.recentTools.length > 0) {
710
+ // Show most recent completed tool when idle between tools
711
+ const recent = progress.recentTools[0];
712
+ let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("dim", sanitizeText(recent.tool))}`;
713
+ const toolDetail = progress.lastIntent ?? recent.args;
714
+ if (toolDetail) {
715
+ toolLine += `: ${theme.fg("dim", previewLine(sanitizeText(toolDetail), 40))}`;
716
+ }
717
+ lines.push(toolLine);
718
+ }
719
+ }
720
+
721
+ // Retry detail line: surface why the subagent is paused and roughly how
722
+ // long until the next attempt. Without this, the parent UI would just
723
+ // keep spinning while a child sleeps on a 3-hour provider rate-limit.
724
+ if (progress.retryState && progress.status === "running") {
725
+ const remainingMs = Math.max(0, progress.retryState.startedAtMs + progress.retryState.delayMs - nowMs);
726
+ const waitLabel = remainingMs > 0 ? `in ${formatDuration(remainingMs)}` : "now";
727
+ const summary =
728
+ `retrying ${progress.retryState.attempt}/${progress.retryState.maxAttempts} ${waitLabel}: ` +
729
+ previewLine(sanitizeText(progress.retryState.errorMessage), 60);
730
+ lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("warning", summary)}`);
731
+ } else if (progress.retryFailure && progress.status !== "running") {
732
+ const summary = `auto-retry gave up after ${progress.retryFailure.attempt} attempt${
733
+ progress.retryFailure.attempt === 1 ? "" : "s"
734
+ }: ${previewLine(sanitizeText(progress.retryFailure.errorMessage), 80)}`;
735
+ lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("error", summary)}`);
736
+ }
737
+
738
+ // Render extracted tool data inline (e.g., review findings)
739
+ if (progress.extractedToolData) {
740
+ // For completed tasks, render review verdicts assembled from incremental
741
+ // yield sections.
742
+ if (progress.status === "completed") {
743
+ const completeData = normalizeYieldData(progress.extractedToolData.yield);
744
+ const incrementalReview = extractIncrementalReviewResult(completeData);
745
+ if (incrementalReview) {
746
+ lines.push(
747
+ ...renderReviewResult(
748
+ incrementalReview.summary,
749
+ incrementalReview.findings,
750
+ continuePrefix,
751
+ expanded,
752
+ theme,
753
+ ),
754
+ );
755
+ return lines; // Review result handles its own rendering
756
+ }
757
+ const reviewData = completeData
758
+ .map(c => c.data as SubmitReviewDetails)
759
+ .filter(d => d && typeof d === "object" && "overall_correctness" in d);
760
+ if (reviewData.length > 0) {
761
+ const summary = reviewData[reviewData.length - 1];
762
+ const findings: FindingDetails[] = [];
763
+ lines.push(...renderReviewResult(summary, findings, continuePrefix, expanded, theme));
764
+ return lines; // Review result handles its own rendering
765
+ }
766
+ }
767
+
768
+ for (const toolName in progress.extractedToolData) {
769
+ const dataArray = progress.extractedToolData[toolName];
770
+ if (toolName === "yield") {
771
+ lines.push(...renderTypedYieldSections(dataArray, continuePrefix, expanded, theme));
772
+ continue;
773
+ }
774
+
775
+ // Nested `task` data has its own dedicated tree renderer below that
776
+ // also merges in the in-flight snapshot — skip the generic inline
777
+ // path so we don't render twice.
778
+ if (toolName === "task") continue;
779
+
780
+ const handler = getSubprocessToolRenderer(toolName);
781
+ if (handler?.renderInline) {
782
+ const displayCount = expanded ? (dataArray as unknown[]).length : 3;
783
+ const recentData = (dataArray as unknown[]).slice(-displayCount);
784
+ for (const data of recentData) {
785
+ const component = handler.renderInline(data, theme);
786
+ if (component instanceof Text) {
787
+ lines.push(`${continuePrefix}${component.getText()}`);
788
+ }
789
+ }
790
+ if ((dataArray as unknown[]).length > displayCount) {
791
+ lines.push(
792
+ `${continuePrefix}${theme.fg(
793
+ "dim",
794
+ formatMoreItems((dataArray as unknown[]).length - displayCount, "item"),
795
+ )}`,
796
+ );
797
+ }
798
+ }
799
+ }
800
+ }
801
+
802
+ // Nested `task` tree: completed sub-calls from `extractedToolData.task` plus
803
+ // the in-flight snapshot (if any). Surfacing this in the live view means
804
+ // the user sees deep-tree progress without waiting for this agent to finish
805
+ // its own turn.
806
+ const completedTaskCalls = (progress.extractedToolData?.task as TaskToolDetails[] | undefined) ?? [];
807
+ const inflight = progress.inflightTaskDetails;
808
+ if (completedTaskCalls.length > 0 || inflight) {
809
+ const snapshots = inflight ? [...completedTaskCalls, inflight] : completedTaskCalls;
810
+ const nestedLines = renderNestedTaskTree(
811
+ snapshots,
812
+ expanded,
813
+ theme,
814
+ spinnerFrame,
815
+ frozen,
816
+ seenNestedTasks,
817
+ nestedDepth,
818
+ nowMs,
819
+ Math.max(0, maxWidth - visibleWidth(continuePrefix)),
820
+ );
821
+ for (const line of nestedLines) {
822
+ lines.push(`${continuePrefix}${line}`);
823
+ }
824
+ }
825
+
826
+ // Expanded view: recent output and tools
827
+ if (expanded && progress.status === "running") {
828
+ const previewRows = previewWindowRows();
829
+ const output = capPreviewLines(
830
+ sanitizeRecentOutput([...progress.recentOutput].reverse().join("\n")).split("\n"),
831
+ theme,
832
+ {
833
+ max: previewRows,
834
+ expandHint: false,
835
+ },
836
+ ).join("\n");
837
+ lines.push(...renderOutputSection(output, continuePrefix, expanded, theme, 2, previewRows));
838
+ }
839
+
840
+ return lines;
841
+ }
842
+
843
+ /**
844
+ * Render review result with combined verdict + findings in tree structure.
845
+ */
846
+ function renderReviewResult(
847
+ summary: SubmitReviewDetails,
848
+ findings: FindingDetails[],
849
+ continuePrefix: string,
850
+ expanded: boolean,
851
+ theme: Theme,
852
+ ): string[] {
853
+ const lines: string[] = [];
854
+
855
+ // Verdict line
856
+ const verdictColor = summary.overall_correctness === "correct" ? "success" : "error";
857
+ const isCorrect = summary.overall_correctness === "correct";
858
+ const verdictIcon = isCorrect
859
+ ? theme.styledSymbol("status.done", "accent")
860
+ : theme.fg(verdictColor, theme.status.error);
861
+ lines.push(
862
+ `${continuePrefix} Patch is ${theme.fg(verdictColor, summary.overall_correctness)} ${verdictIcon} ${theme.fg(
863
+ "dim",
864
+ `(${(summary.confidence * 100).toFixed(0)}% confidence)`,
865
+ )}`,
866
+ );
867
+
868
+ // Explanation preview (first ~80 chars when collapsed, full when expanded)
869
+ if (summary.explanation) {
870
+ if (expanded) {
871
+ lines.push(`${continuePrefix}${theme.fg("dim", "Summary")}`);
872
+ const explanationLines = sanitizeText(summary.explanation).split("\n");
873
+ for (const line of explanationLines) {
874
+ lines.push(`${continuePrefix} ${theme.fg("dim", replaceTabs(line))}`);
875
+ }
876
+ } else {
877
+ // Preview: first sentence or ~100 chars (flatten tabs/newlines first)
878
+ const flat = replaceTabs(sanitizeText(summary.explanation)).replace(/[\r\n]+/g, " ");
879
+ const firstSentence = flat.split(/[.!?]/)[0].trim();
880
+ const preview = truncateToWidth(`${firstSentence}.`, 100);
881
+ lines.push(`${continuePrefix}${theme.fg("dim", preview)}`);
882
+ }
883
+ }
884
+
885
+ // Findings summary + list
886
+ lines.push(`${continuePrefix}${formatFindingSummary(findings, theme)}`);
887
+
888
+ if (findings.length > 0) {
889
+ lines.push(...renderFindings(findings, continuePrefix, expanded, theme));
890
+ }
891
+
892
+ return lines;
893
+ }
894
+
895
+ /**
896
+ * Render review findings list.
897
+ */
898
+ function renderFindings(findings: FindingDetails[], continuePrefix: string, expanded: boolean, theme: Theme): string[] {
899
+ const lines: string[] = [];
900
+
901
+ // Sort by priority (lower = more severe) when collapsed to show most important first
902
+ const sortedFindings = expanded
903
+ ? findings
904
+ : [...findings].sort((a, b) => getPriorityInfo(a.priority).ord - getPriorityInfo(b.priority).ord);
905
+ const displayCount = expanded ? sortedFindings.length : Math.min(3, sortedFindings.length);
906
+
907
+ for (let i = 0; i < displayCount; i++) {
908
+ const finding = sortedFindings[i];
909
+ const isLastFinding = i === displayCount - 1 && (expanded || sortedFindings.length <= 3);
910
+ const findingPrefix = isLastFinding ? theme.tree.last : theme.tree.branch;
911
+ const findingContinue = isLastFinding ? " " : `${theme.tree.vertical} `;
912
+
913
+ const { color } = getPriorityInfo(finding.priority);
914
+ const rawTitle = sanitizeText(finding.title?.replace(/^\[P\d\]\s*/, "") ?? "Untitled");
915
+ const titleText = replaceTabs(rawTitle).replace(/[\r\n]+/g, " ");
916
+ const loc = `${path.basename(sanitizeText(finding.file_path || "<unknown>"))}:${finding.line_start}`;
917
+
918
+ lines.push(
919
+ `${continuePrefix}${findingPrefix} ${theme.fg(color, `[${finding.priority}]`)} ${titleText} ${theme.fg("dim", loc)}`,
920
+ );
921
+
922
+ // Show body when expanded
923
+ if (expanded && finding.body) {
924
+ // Wrap body text
925
+ const bodyLines = sanitizeText(finding.body).split("\n");
926
+ for (const bodyLine of bodyLines) {
927
+ lines.push(`${continuePrefix}${findingContinue}${theme.fg("dim", replaceTabs(bodyLine))}`);
928
+ }
929
+ }
930
+ }
931
+
932
+ if (!expanded && findings.length > 3) {
933
+ lines.push(`${continuePrefix}${theme.fg("dim", formatMoreItems(findings.length - 3, "finding"))}`);
934
+ }
935
+
936
+ return lines;
937
+ }
938
+
939
+ /**
940
+ * Render final result for a single agent.
941
+ */
942
+ function renderAgentResult(
943
+ result: SingleResult,
944
+ prefix: string,
945
+ continuePrefix: string,
946
+ expanded: boolean,
947
+ theme: Theme,
948
+ seenNestedTasks?: WeakSet<object>,
949
+ nestedDepth = 0,
950
+ maxWidth = Number.POSITIVE_INFINITY,
951
+ ): string[] {
952
+ const lines: string[] = [];
953
+
954
+ const { warning: missingCompleteWarning, rest: outputWithoutWarning } = extractMissingYieldWarning(result.output);
955
+ const aborted = result.aborted ?? false;
956
+ const mergeFailed = !aborted && result.exitCode === 0 && !!result.error;
957
+ const success = !aborted && result.exitCode === 0 && !result.error;
958
+ const needsWarning = Boolean(missingCompleteWarning) && success;
959
+ const icon = aborted
960
+ ? theme.status.aborted
961
+ : needsWarning
962
+ ? theme.status.warning
963
+ : success
964
+ ? theme.styledSymbol("status.done", "text")
965
+ : theme.status.error;
966
+ const iconColor = needsWarning ? "warning" : success ? "success" : mergeFailed ? "warning" : "error";
967
+ const statusText = aborted
968
+ ? "aborted"
969
+ : needsWarning
970
+ ? "warning"
971
+ : success
972
+ ? "done"
973
+ : mergeFailed
974
+ ? "merge failed"
975
+ : "failed";
976
+
977
+ // Reserve the name and required badges before optional model metadata and details.
978
+ const fullDescription = result.description ? replaceTabs(sanitizeText(result.description)).trim() : undefined;
979
+ const indent = prefix ? `${prefix} ` : "";
980
+ const statusBadge = ` ${formatBadge(statusText, iconColor, theme)}`;
981
+ const displayId = truncateTaskRow(
982
+ formatTaskId(result.id),
983
+ Math.max(0, maxWidth - visibleWidth(`${indent}${icon} ${statusBadge}`)),
984
+ );
985
+ const roleBadge = truncateTaskRow(
986
+ agentTypeBadge(result.agent, theme),
987
+ Math.max(0, maxWidth - visibleWidth(`${indent}${icon} ${displayId}${statusBadge}`)),
988
+ );
989
+ const badges = `${roleBadge}${statusBadge}`;
990
+ const modelBadge = isFeedModelBadgeEnabled()
991
+ ? formatFeedModelBadge(
992
+ result.resolvedModelIdentity ?? result.resolvedModel,
993
+ result.resolvedThinkingLevel,
994
+ result.advisor,
995
+ theme,
996
+ Math.min(
997
+ FEED_MODEL_BADGE_WIDTH,
998
+ Math.max(0, maxWidth - visibleWidth(`${indent}${icon} ${displayId}${badges}`) - 1),
999
+ ),
1000
+ )
1001
+ : "";
1002
+ const modelLead = modelBadge ? `${modelBadge} ` : "";
1003
+ const description =
1004
+ fullDescription &&
1005
+ visibleWidth(`${indent}${icon} ${modelLead}${displayId}: ${fullDescription}${badges}`) <= maxWidth
1006
+ ? fullDescription
1007
+ : undefined;
1008
+ const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
1009
+ let statusLine = `${indent}${theme.fg(iconColor, icon)} ${modelLead}${theme.fg(
1010
+ success && !needsWarning ? "text" : "accent",
1011
+ titlePart,
1012
+ )}${badges}`;
1013
+ statusLine += formatAgentStatRun(
1014
+ {
1015
+ requests: result.requests,
1016
+ contextTokens: result.contextTokens,
1017
+ contextWindow: result.contextWindow,
1018
+ cost: result.usage?.cost.total ?? 0,
1019
+ },
1020
+ theme,
1021
+ );
1022
+ statusLine += `${theme.sep.dot}${theme.fg("dim", formatDuration(result.durationMs))}`;
1023
+
1024
+ if (result.truncated) {
1025
+ statusLine += ` ${theme.fg("warning", "[truncated]")}`;
1026
+ }
1027
+
1028
+ lines.push(truncateTaskRow(statusLine, maxWidth, ""));
1029
+ if (fullDescription && !description) {
1030
+ lines.push(...renderDescriptionLines(fullDescription, continuePrefix, maxWidth, theme));
1031
+ }
1032
+
1033
+ lines.push(...renderTaskSection(result.assignment ?? result.task, continuePrefix, expanded, theme));
1034
+
1035
+ if (aborted && result.abortReason) {
1036
+ lines.push(
1037
+ `${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg(
1038
+ "dim",
1039
+ previewLine(sanitizeText(result.abortReason), 80),
1040
+ )}`,
1041
+ );
1042
+ }
1043
+ // Check for review result from incremental yield sections.
1044
+ // `normalizeYieldData` guards against a stray non-array `yield` slot —
1045
+ // optional chaining on `.map` only short-circuits on null/undefined and
1046
+ // would otherwise crash the renderer with `TypeError: completeData?.map
1047
+ // is not a function` when the slot is a plain object (see issue #1987).
1048
+ const completeData = normalizeYieldData(result.extractedToolData?.yield);
1049
+ const incrementalReview = extractIncrementalReviewResult(completeData);
1050
+
1051
+ if (incrementalReview) {
1052
+ lines.push(
1053
+ ...renderReviewResult(incrementalReview.summary, incrementalReview.findings, continuePrefix, expanded, theme),
1054
+ );
1055
+ return lines;
1056
+ }
1057
+
1058
+ // Extract review verdict from legacy yield summary objects if present.
1059
+ const reviewData = completeData
1060
+ .map(c => c.data as SubmitReviewDetails)
1061
+ .filter(d => d && typeof d === "object" && "overall_correctness" in d);
1062
+ const submitReviewData = reviewData.length > 0 ? reviewData : undefined;
1063
+
1064
+ if (submitReviewData) {
1065
+ const summary = submitReviewData[submitReviewData.length - 1];
1066
+ const findings: FindingDetails[] = [];
1067
+ lines.push(...renderReviewResult(summary, findings, continuePrefix, expanded, theme));
1068
+ return lines;
1069
+ }
1070
+
1071
+ // Check for extracted tool data with custom renderers (skip review tools)
1072
+ let hasCustomRendering = false;
1073
+ const deferredToolLines: string[] = [];
1074
+ if (result.extractedToolData) {
1075
+ for (const toolName in result.extractedToolData) {
1076
+ const dataArray = result.extractedToolData[toolName];
1077
+ if (toolName === "yield") {
1078
+ const yieldLines = renderTypedYieldSections(dataArray, continuePrefix, expanded, theme);
1079
+ if (yieldLines.length > 0) {
1080
+ hasCustomRendering = true;
1081
+ lines.push(...yieldLines);
1082
+ }
1083
+ continue;
1084
+ }
1085
+
1086
+ const isTaskTool = toolName === "task";
1087
+ if (isTaskTool && (dataArray as unknown[]).length > 0) {
1088
+ for (const line of renderNestedTaskResults(
1089
+ dataArray as TaskToolDetails[],
1090
+ expanded,
1091
+ theme,
1092
+ seenNestedTasks,
1093
+ nestedDepth,
1094
+ Math.max(0, maxWidth - visibleWidth(continuePrefix)),
1095
+ )) {
1096
+ deferredToolLines.push(`${continuePrefix}${line}`);
1097
+ }
1098
+ continue;
1099
+ }
1100
+
1101
+ const handler = getSubprocessToolRenderer(toolName);
1102
+ if (handler?.renderFinal && (dataArray as unknown[]).length > 0) {
1103
+ const component = handler.renderFinal(dataArray as unknown[], theme, expanded);
1104
+ const target = lines;
1105
+ if (!isTaskTool) {
1106
+ hasCustomRendering = true;
1107
+ target.push(`${continuePrefix}${theme.fg("dim", `Tool: ${toolName}`)}`);
1108
+ }
1109
+ if (component instanceof Text) {
1110
+ // Prefix each line with continuePrefix
1111
+ const text = component.getText();
1112
+ for (const line of text.split("\n")) {
1113
+ target.push(`${continuePrefix}${line}`);
1114
+ }
1115
+ } else if (component instanceof Container) {
1116
+ // For containers, render each child
1117
+ for (const child of (component as Container).children) {
1118
+ if (child instanceof Text) {
1119
+ target.push(`${continuePrefix}${child.getText()}`);
1120
+ }
1121
+ }
1122
+ }
1123
+ }
1124
+ }
1125
+ }
1126
+
1127
+ if (hasCustomRendering && missingCompleteWarning) {
1128
+ lines.push(
1129
+ `${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg(
1130
+ "dim",
1131
+ truncateToWidth(sanitizeText(missingCompleteWarning), 80),
1132
+ )}`,
1133
+ );
1134
+ }
1135
+
1136
+ // Fallback to output preview if no custom rendering
1137
+ if (!hasCustomRendering) {
1138
+ lines.push(
1139
+ ...renderOutputSection(outputWithoutWarning, continuePrefix, expanded, theme, 3, 12, missingCompleteWarning),
1140
+ );
1141
+ }
1142
+
1143
+ if (deferredToolLines.length > 0) {
1144
+ lines.push(...deferredToolLines);
1145
+ }
1146
+
1147
+ // Artifact rows: paths shortened (home → `~`), tabs expanded, and width-bounded
1148
+ // like every other rendered line; the full paths live in the model-facing summary.
1149
+ // A nested-only run still carries its (empty) root patch path, so hide that
1150
+ // row when the runner reports no root changes — same as the model summary.
1151
+ if (result.patchPath && result.hasRootChanges !== false && !aborted && result.exitCode === 0) {
1152
+ lines.push(
1153
+ `${continuePrefix}${theme.fg("dim", truncateToWidth(`Patch: ${replaceTabs(shortenPath(result.patchPath))}`, TRUNCATE_LENGTHS.CONTENT))}`,
1154
+ );
1155
+ } else if (result.branchName && !aborted && result.exitCode === 0) {
1156
+ lines.push(
1157
+ `${continuePrefix}${theme.fg("dim", truncateToWidth(`Branch: ${replaceTabs(sanitizeText(result.branchName))}`, TRUNCATE_LENGTHS.CONTENT))}`,
1158
+ );
1159
+ }
1160
+ if (!aborted && result.exitCode === 0) {
1161
+ for (const nestedPath of result.nestedPatchPaths ?? []) {
1162
+ lines.push(
1163
+ `${continuePrefix}${theme.fg("dim", truncateToWidth(`Nested patch: ${replaceTabs(shortenPath(nestedPath))}`, TRUNCATE_LENGTHS.CONTENT))}`,
1164
+ );
1165
+ }
1166
+ }
1167
+
1168
+ // Error message
1169
+ if (result.error && (!success || mergeFailed) && (!aborted || result.error !== result.abortReason)) {
1170
+ lines.push(
1171
+ `${continuePrefix}${theme.fg(mergeFailed ? "warning" : "error", previewLine(sanitizeText(result.error), 70))}`,
1172
+ );
1173
+ }
1174
+
1175
+ return lines;
1176
+ }
1177
+
1178
+ /**
1179
+ * Order live progress entries so finished agents render first — sorted by
1180
+ * runtime ascending, matching {@link orderResultsForDisplay} — while
1181
+ * unfinished (pending/running) ones stay pinned at the bottom in dispatch
1182
+ * order. Because a finished agent's runtime is fixed, finalization renders
1183
+ * the same order and rows never reshuffle.
1184
+ */
1185
+ function orderProgressForDisplay(progress: readonly AgentProgress[]): AgentProgress[] {
1186
+ const finished: AgentProgress[] = [];
1187
+ const unfinished: AgentProgress[] = [];
1188
+ for (const p of progress) {
1189
+ (p.status === "pending" || p.status === "running" ? unfinished : finished).push(p);
1190
+ }
1191
+ finished.sort((a, b) => a.durationMs - b.durationMs || a.index - b.index);
1192
+ return finished.concat(unfinished);
1193
+ }
1194
+
1195
+ /**
1196
+ * Order finalized results by runtime ascending (tie-break: dispatch index) so
1197
+ * the finalized list matches the live-progress order produced by
1198
+ * {@link orderProgressForDisplay}.
1199
+ */
1200
+ function orderResultsForDisplay(results: readonly SingleResult[]): SingleResult[] {
1201
+ return [...results].sort((a, b) => a.durationMs - b.durationMs || a.index - b.index);
1202
+ }
1203
+
1204
+ /**
1205
+ * Summary line for progress rows folded away by the collapsed cap: per-status
1206
+ * counts plus the expand hint, e.g. `… 21 more agents (18 pending · 3 done)`.
1207
+ */
1208
+ function formatHiddenProgressLine(hidden: readonly AgentProgress[], theme: Theme): string {
1209
+ const counts: Record<AgentProgress["status"], number> = {
1210
+ pending: 0,
1211
+ running: 0,
1212
+ completed: 0,
1213
+ failed: 0,
1214
+ aborted: 0,
1215
+ };
1216
+ for (const p of hidden) counts[p.status]++;
1217
+ const parts: string[] = [];
1218
+ if (counts.completed > 0) parts.push(theme.fg("dim", `${counts.completed} done`));
1219
+ if (counts.running > 0) parts.push(theme.fg("dim", `${counts.running} running`));
1220
+ if (counts.pending > 0) parts.push(theme.fg("dim", `${counts.pending} pending`));
1221
+ if (counts.failed > 0) parts.push(theme.fg("error", `${counts.failed} failed`));
1222
+ if (counts.aborted > 0) parts.push(theme.fg("error", `${counts.aborted} aborted`));
1223
+ const breakdown =
1224
+ parts.length > 0
1225
+ ? `${theme.fg("dim", " (")}${parts.join(theme.fg("dim", theme.sep.dot))}${theme.fg("dim", ")")}`
1226
+ : "";
1227
+ const hint = formatExpandHint(theme, false, true);
1228
+ return `${theme.fg("dim", formatMoreItems(hidden.length, "agent"))}${breakdown}${hint ? ` ${hint}` : ""}`;
1229
+ }
1230
+
1231
+ /**
1232
+ * Pick the agent rows that stay visible when a finalized batch is collapsed:
1233
+ * problem rows (aborted/failed/merge-failed) claim slots first so they are
1234
+ * never folded away, then fastest finishers fill the remainder. The pick is
1235
+ * filtered out of the display order, so visible rows keep the expanded layout.
1236
+ */
1237
+ function selectCollapsedResults(ordered: readonly SingleResult[]): readonly SingleResult[] {
1238
+ if (ordered.length <= COLLAPSED_AGENT_LIMIT) return ordered;
1239
+ const picked = new Set<SingleResult>();
1240
+ for (const result of ordered) {
1241
+ if (picked.size >= COLLAPSED_AGENT_LIMIT) break;
1242
+ if (result.aborted || result.exitCode !== 0 || result.error) picked.add(result);
1243
+ }
1244
+ for (const result of ordered) {
1245
+ if (picked.size >= COLLAPSED_AGENT_LIMIT) break;
1246
+ picked.add(result);
1247
+ }
1248
+ return ordered.filter(result => picked.has(result));
1249
+ }
1250
+
1251
+ /**
1252
+ * Render the tool result.
1253
+ */
1254
+ export function renderResult(
1255
+ result: { content: Array<{ type: string; text?: string }>; details?: TaskToolDetails; isError?: boolean },
1256
+ options: TaskRenderOptions,
1257
+ theme: Theme,
1258
+ args?: TaskParams,
1259
+ ): Component {
1260
+ const fallbackText = result.content.find(c => c.type === "text")?.text ?? "";
1261
+ const details = result.details;
1262
+ const agentLabel = formatAgentHeaderLabel(args);
1263
+ const assignmentSection = createAssignmentSectionRenderer(args, theme);
1264
+ const contextSection = createContextSectionRenderer(args, theme);
1265
+
1266
+ if (!details) {
1267
+ const text = result.content.find(c => c.type === "text")?.text || "";
1268
+ const errored = result.isError === true;
1269
+ const header = errored
1270
+ ? renderStatusLine({ icon: "error", title: "Task", description: agentLabel }, theme)
1271
+ : renderStatusLine(
1272
+ {
1273
+ iconOverride: theme.styledSymbol("status.done", "accent"),
1274
+ title: "Task",
1275
+ description: agentLabel,
1276
+ },
1277
+ theme,
1278
+ );
1279
+ return framedToolCard(theme, ({ width }) => ({
1280
+ header,
1281
+ sections: [
1282
+ ...(contextSection ? [contextSection(width)] : []),
1283
+ ...(assignmentSection ? [assignmentSection(width)] : []),
1284
+ ...(text ? [{ separator: true, content: [theme.fg("dim", truncateToWidth(text, width))] }] : []),
1285
+ ],
1286
+ phase: errored ? "error" : "success",
1287
+ borderColor: errored ? "error" : "borderMuted",
1288
+ }));
1289
+ }
1290
+
1291
+ const hasResults = Boolean(details.results && details.results.length > 0);
1292
+ // Single pass over details.results derives the header booleans AND the footer
1293
+ // counts/totals. This block re-runs ~30×/sec via the 33ms spinner render; the
1294
+ // previous form did 3× `.some()` here plus 3× `.filter()` + `.reduce()` again
1295
+ // inside the frame below (7+ full passes per tick).
1296
+ let abortedCount = 0;
1297
+ let failCount = 0;
1298
+ let mergeFailedCount = 0;
1299
+ let successCount = 0;
1300
+ let requestTotal = 0;
1301
+ if (hasResults) {
1302
+ for (const r of details.results) {
1303
+ requestTotal += r.requests ?? 0;
1304
+ if (r.aborted) abortedCount++;
1305
+ else if (r.exitCode !== 0) failCount++;
1306
+ else if (r.error) mergeFailedCount++;
1307
+ else successCount++;
1308
+ }
1309
+ }
1310
+ const aborted = abortedCount > 0;
1311
+ const failed = failCount > 0;
1312
+ const mergeFailed = mergeFailedCount > 0;
1313
+ const isError = aborted || failed;
1314
+ const agentCount = hasResults ? details.results.length : (details.progress?.length ?? 0);
1315
+ const icon: ToolUIStatus = options.isPartial ? "running" : isError ? "error" : mergeFailed ? "warning" : "success";
1316
+ // Header meta is the spawn count only; each row carries its own ⟨agent⟩
1317
+ // badge, so a joined type list here would repeat them. Before anything
1318
+ // spawns, fall back to the flat form's agent type from the call args.
1319
+ const countLabel = agentCount > 0 ? `${agentCount} ${agentCount === 1 ? "agent" : "agents"}` : undefined;
1320
+ const metaLabel = countLabel ?? agentLabel;
1321
+ const header = renderStatusLine(
1322
+ {
1323
+ icon: icon === "success" || icon === "running" ? undefined : icon,
1324
+ // While agents are in flight the header shows the dispatch glyph, not a
1325
+ // spinner: async spawns return immediately, so "running" means
1326
+ // "delegated to peers", not "this call is blocking the turn".
1327
+ iconOverride:
1328
+ icon === "running"
1329
+ ? theme.styledSymbol("tool.task", "accent")
1330
+ : icon === "success"
1331
+ ? theme.styledSymbol("status.done", "accent")
1332
+ : undefined,
1333
+ title: "Task",
1334
+ meta: metaLabel ? [metaLabel] : undefined,
1335
+ },
1336
+ theme,
1337
+ );
1338
+
1339
+ return framedToolCard(theme, ({ width, contentWidth }) => {
1340
+ const { expanded, isPartial, spinnerFrame } = options;
1341
+ const frozen = options.renderContext?.frozen === true;
1342
+ const nowMs = options.renderContext?.nowMs ?? Date.now();
1343
+ const lines: string[] = [];
1344
+
1345
+ // Result rows win once any exist; progress rows for spawns without a
1346
+ // result (a mixed call's async subset) render as a supplement below.
1347
+ const shouldRenderProgress =
1348
+ Boolean(details.progress && details.progress.length > 0) && details.results.length === 0;
1349
+ if (shouldRenderProgress && details.progress) {
1350
+ const ordered = orderProgressForDisplay(details.progress);
1351
+ // Collapsed view keeps the live edge: finished rows sort to the top of
1352
+ // the display order, so folding from the top keeps running/pending
1353
+ // agents (and their current-tool lines) visible while one summary line
1354
+ // stands in for everything above it.
1355
+ const visible = expanded ? ordered : ordered.slice(Math.max(0, ordered.length - COLLAPSED_AGENT_LIMIT));
1356
+ if (visible.length < ordered.length) {
1357
+ lines.push(formatHiddenProgressLine(ordered.slice(0, ordered.length - visible.length), theme));
1358
+ }
1359
+ for (const progress of visible) {
1360
+ lines.push(
1361
+ ...renderAgentProgress(
1362
+ progress,
1363
+ "",
1364
+ " ",
1365
+ expanded,
1366
+ theme,
1367
+ spinnerFrame,
1368
+ frozen,
1369
+ undefined,
1370
+ 0,
1371
+ nowMs,
1372
+ contentWidth,
1373
+ ),
1374
+ );
1375
+ }
1376
+ } else if (details.results && details.results.length > 0) {
1377
+ const ordered = orderResultsForDisplay(details.results);
1378
+ const visible = expanded ? ordered : selectCollapsedResults(ordered);
1379
+ for (const res of visible) {
1380
+ lines.push(...renderAgentResult(res, "", " ", expanded, theme, undefined, 0, contentWidth));
1381
+ }
1382
+ if (visible.length < ordered.length) {
1383
+ const hint = formatExpandHint(theme, false, true);
1384
+ lines.push(
1385
+ `${theme.fg("dim", formatMoreItems(ordered.length - visible.length, "agent"))}${hint ? ` ${hint}` : ""}`,
1386
+ );
1387
+ }
1388
+
1389
+ // Mixed blocking+async call: async spawns never land in `results`
1390
+ // (their payloads deliver through jobs) — keep their rows visible
1391
+ // beside the finalized inline results, live while running and
1392
+ // settled once their jobs finish.
1393
+ const supplementalProgress = details.progress
1394
+ ? orderProgressForDisplay(
1395
+ details.progress.filter(progress => !details.results.some(res => res.id === progress.id)),
1396
+ )
1397
+ : [];
1398
+ for (const progress of supplementalProgress) {
1399
+ lines.push(
1400
+ ...renderAgentProgress(
1401
+ progress,
1402
+ "",
1403
+ " ",
1404
+ expanded,
1405
+ theme,
1406
+ spinnerFrame,
1407
+ frozen,
1408
+ undefined,
1409
+ 0,
1410
+ nowMs,
1411
+ contentWidth,
1412
+ ),
1413
+ );
1414
+ }
1415
+
1416
+ const summaryParts: string[] = [];
1417
+ if (abortedCount > 0) summaryParts.push(theme.fg("error", `${abortedCount} aborted`));
1418
+ if (successCount > 0) summaryParts.push(theme.fg("success", `${successCount} succeeded`));
1419
+ if (mergeFailedCount > 0) summaryParts.push(theme.fg("warning", `${mergeFailedCount} merge failed`));
1420
+ if (failCount > 0) summaryParts.push(theme.fg("error", `${failCount} failed`));
1421
+ const totalRequests = requestTotal;
1422
+ if (totalRequests > 0) summaryParts.push(theme.fg("dim", `${formatNumber(totalRequests)} req`));
1423
+ summaryParts.push(theme.fg("dim", formatDuration(details.totalDurationMs)));
1424
+ // Wrap the run summary in the theme's bracket glyphs (dim chrome, colored
1425
+ // counts) to match the bash tool's `[Wall: … | Exit: …]` footer.
1426
+ lines.push(
1427
+ theme.fg("dim", theme.format.bracketLeft) +
1428
+ summaryParts.join(theme.fg("dim", theme.sep.dot)) +
1429
+ theme.fg("dim", theme.format.bracketRight),
1430
+ );
1431
+ }
1432
+
1433
+ const phase = isPartial ? "partial" : isError ? "error" : mergeFailed ? "warning" : "success";
1434
+ const borderColor = isError ? "error" : "borderMuted";
1435
+
1436
+ if (lines.length === 0) {
1437
+ const text = fallbackText.trim() ? fallbackText : "No results";
1438
+ return {
1439
+ header,
1440
+ sections: [
1441
+ ...(contextSection ? [contextSection(width)] : []),
1442
+ ...(assignmentSection ? [assignmentSection(width)] : []),
1443
+ { separator: true, content: [theme.fg("dim", truncateToWidth(text, width))] },
1444
+ ],
1445
+ phase,
1446
+ borderColor,
1447
+ };
1448
+ }
1449
+
1450
+ if (fallbackText.trim()) {
1451
+ const summaryLines = fallbackText.split("\n");
1452
+ const markerIndex = summaryLines.findIndex(
1453
+ line =>
1454
+ line.includes("<system-notification>") ||
1455
+ line.startsWith("Applied patches:") ||
1456
+ line.startsWith("No changes to apply."),
1457
+ );
1458
+ if (markerIndex >= 0) {
1459
+ const extra = summaryLines.slice(markerIndex);
1460
+ for (const line of extra) {
1461
+ if (!line.trim()) continue;
1462
+ lines.push(theme.fg("dim", line));
1463
+ }
1464
+ }
1465
+ }
1466
+
1467
+ while (lines.length > 0 && lines[0].trim() === "") lines.shift();
1468
+ return {
1469
+ header,
1470
+ sections: [
1471
+ ...(contextSection ? [contextSection(width)] : []),
1472
+ ...(assignmentSection ? [assignmentSection(width)] : []),
1473
+ ...(lines.length > 0 ? [{ separator: true, content: lines }] : []),
1474
+ ],
1475
+ phase,
1476
+ borderColor,
1477
+ };
1478
+ });
1479
+ }
1480
+
1481
+ /** Tests whether a persisted tool result carries a task snapshot. */
1482
+ export function isTaskToolDetails(value: unknown): value is TaskToolDetails {
1483
+ return (
1484
+ Boolean(value) &&
1485
+ typeof value === "object" &&
1486
+ "results" in (value as TaskToolDetails) &&
1487
+ Array.isArray((value as TaskToolDetails).results)
1488
+ );
1489
+ }
1490
+
1491
+ /**
1492
+ * Subagent ids visible on a task tool card, for click-to-focus hit-testing.
1493
+ * Reads `progress[]` (in-flight) and `results[]` (settled) defensively — card
1494
+ * details arrive as `unknown` through the tool-result pipeline — and recurses
1495
+ * into the same nested snapshots the card renders (`extractedToolData.task`
1496
+ * details plus the in-flight snapshot), so a click on a nested worker row
1497
+ * resolves to that worker instead of an outer agent. Callers intersect with
1498
+ * the live registry, which decides focusability and recency.
1499
+ */
1500
+ export function taskCardAgentIds(details: unknown): string[] {
1501
+ if (typeof details !== "object" || details === null) return [];
1502
+ const ids: string[] = [];
1503
+ const seen = new Set<unknown>();
1504
+ const pushId = (item: unknown): void => {
1505
+ if (typeof item !== "object" || item === null || !("id" in item)) return;
1506
+ const id: unknown = item.id;
1507
+ if (typeof id === "string" && id.length > 0 && !ids.includes(id)) ids.push(id);
1508
+ };
1509
+ const collectDetails = (value: unknown, depth: number): void => {
1510
+ if (typeof value !== "object" || value === null || depth > MAX_NESTED_TASK_RENDER_DEPTH) return;
1511
+ if (seen.has(value)) return;
1512
+ seen.add(value);
1513
+ const record = value as { results?: unknown; progress?: unknown };
1514
+ collectList(record.results, depth);
1515
+ collectList(record.progress, depth);
1516
+ };
1517
+ const collectList = (value: unknown, depth: number): void => {
1518
+ if (!Array.isArray(value)) return;
1519
+ for (const item of value) {
1520
+ pushId(item);
1521
+ if (typeof item !== "object" || item === null || seen.has(item)) continue;
1522
+ seen.add(item);
1523
+ const record = item as { extractedToolData?: unknown; inflightTaskDetails?: unknown };
1524
+ const nested = record.extractedToolData;
1525
+ if (typeof nested === "object" && nested !== null && "task" in nested) {
1526
+ const tasks = (nested as { task?: unknown }).task;
1527
+ if (Array.isArray(tasks)) for (const task of tasks) collectDetails(task, depth + 1);
1528
+ }
1529
+ collectDetails(record.inflightTaskDetails, depth + 1);
1530
+ }
1531
+ };
1532
+ collectList("progress" in details ? details.progress : undefined, 0);
1533
+ collectList("results" in details ? details.results : undefined, 0);
1534
+ return ids;
1535
+ }
1536
+
1537
+ // Nested subagent snapshots sit one or more levels below the frame border, so
1538
+ // they keep tree guides to convey depth (the parent prepends its own continue
1539
+ // prefix). Only the top-level agent list drops guides (the frame is its box).
1540
+ function nestedMarkers(isLast: boolean, theme: Theme): { prefix: string; continuePrefix: string } {
1541
+ return {
1542
+ prefix: isLast ? theme.fg("dim", theme.tree.last) : theme.fg("dim", theme.tree.branch),
1543
+ continuePrefix: isLast ? " " : `${theme.fg("dim", theme.tree.vertical)} `,
1544
+ };
1545
+ }
1546
+
1547
+ /** Renders settled child-task snapshots with bounded recursion and cycle detection. */
1548
+ export function renderNestedTaskResults(
1549
+ detailsList: TaskToolDetails[],
1550
+ expanded: boolean,
1551
+ theme: Theme,
1552
+ seen: WeakSet<object> = new WeakSet<object>(),
1553
+ depth = 0,
1554
+ maxWidth = Number.POSITIVE_INFINITY,
1555
+ ): string[] {
1556
+ const lines: string[] = [];
1557
+ for (const details of detailsList) {
1558
+ if (seen.has(details)) {
1559
+ lines.push(renderNestedCycleLine(theme));
1560
+ continue;
1561
+ }
1562
+ if (depth >= MAX_NESTED_TASK_RENDER_DEPTH) {
1563
+ lines.push(theme.fg("dim", "… nested task depth limit reached"));
1564
+ continue;
1565
+ }
1566
+ seen.add(details);
1567
+ if (!details.results || details.results.length === 0) {
1568
+ seen.delete(details);
1569
+ continue;
1570
+ }
1571
+ const ordered = orderResultsForDisplay(details.results);
1572
+ const visible = expanded ? ordered : selectCollapsedResults(ordered);
1573
+ const hiddenCount = ordered.length - visible.length;
1574
+ visible.forEach((result, index) => {
1575
+ const { prefix, continuePrefix } = nestedMarkers(hiddenCount === 0 && index === visible.length - 1, theme);
1576
+ lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme, seen, depth + 1, maxWidth));
1577
+ });
1578
+ if (hiddenCount > 0) {
1579
+ const { prefix } = nestedMarkers(true, theme);
1580
+ lines.push(`${prefix} ${theme.fg("dim", formatMoreItems(hiddenCount, "agent"))}`);
1581
+ }
1582
+ seen.delete(details);
1583
+ }
1584
+ return lines;
1585
+ }
1586
+
1587
+ /**
1588
+ * Render a list of `TaskToolDetails` snapshots — completed (`results[]`) or
1589
+ * in-flight (`progress[]`) — as an interleaved tree. Used by the live progress
1590
+ * view to surface nested subagent activity while this agent is still running.
1591
+ */
1592
+ function renderNestedTaskTree(
1593
+ detailsList: TaskToolDetails[],
1594
+ expanded: boolean,
1595
+ theme: Theme,
1596
+ spinnerFrame?: number,
1597
+ frozen = false,
1598
+ seen: WeakSet<object> = new WeakSet<object>(),
1599
+ depth = 0,
1600
+ nowMs = Date.now(),
1601
+ maxWidth = Number.POSITIVE_INFINITY,
1602
+ ): string[] {
1603
+ const lines: string[] = [];
1604
+ for (const details of detailsList) {
1605
+ if (seen.has(details)) {
1606
+ lines.push(renderNestedCycleLine(theme));
1607
+ continue;
1608
+ }
1609
+ if (depth >= MAX_NESTED_TASK_RENDER_DEPTH) {
1610
+ lines.push(theme.fg("dim", "… nested task depth limit reached"));
1611
+ continue;
1612
+ }
1613
+ seen.add(details);
1614
+ const hasResults = Boolean(details.results && details.results.length > 0);
1615
+ if (hasResults) {
1616
+ const ordered = orderResultsForDisplay(details.results);
1617
+ const visible = expanded ? ordered : selectCollapsedResults(ordered);
1618
+ const hiddenCount = ordered.length - visible.length;
1619
+ visible.forEach((result, index) => {
1620
+ const { prefix, continuePrefix } = nestedMarkers(hiddenCount === 0 && index === visible.length - 1, theme);
1621
+ lines.push(
1622
+ ...renderAgentResult(result, prefix, continuePrefix, expanded, theme, seen, depth + 1, maxWidth),
1623
+ );
1624
+ });
1625
+ if (hiddenCount > 0) {
1626
+ const { prefix } = nestedMarkers(true, theme);
1627
+ lines.push(`${prefix} ${theme.fg("dim", formatMoreItems(hiddenCount, "agent"))}`);
1628
+ }
1629
+ seen.delete(details);
1630
+ continue;
1631
+ }
1632
+ const inflight = details.progress;
1633
+ if (inflight && inflight.length > 0) {
1634
+ const ordered = orderProgressForDisplay(inflight);
1635
+ const visible = expanded ? ordered : ordered.slice(Math.max(0, ordered.length - COLLAPSED_AGENT_LIMIT));
1636
+ const hiddenCount = ordered.length - visible.length;
1637
+ visible.forEach((prog, index) => {
1638
+ const { prefix, continuePrefix } = nestedMarkers(hiddenCount === 0 && index === visible.length - 1, theme);
1639
+ lines.push(
1640
+ ...renderAgentProgress(
1641
+ prog,
1642
+ prefix,
1643
+ continuePrefix,
1644
+ expanded,
1645
+ theme,
1646
+ spinnerFrame,
1647
+ frozen,
1648
+ seen,
1649
+ depth + 1,
1650
+ nowMs,
1651
+ maxWidth,
1652
+ ),
1653
+ );
1654
+ });
1655
+ if (hiddenCount > 0) {
1656
+ const { prefix } = nestedMarkers(true, theme);
1657
+ lines.push(`${prefix} ${theme.fg("dim", formatMoreItems(hiddenCount, "agent"))}`);
1658
+ }
1659
+ }
1660
+ seen.delete(details);
1661
+ }
1662
+ return lines;
1663
+ }
1664
+
1665
+ /** Renders task calls and their live or settled agent results. */
1666
+ export const taskToolRenderer = { renderCall, renderResult, mergeCallAndResult: true } satisfies ToolRenderer<
1667
+ TaskParams,
1668
+ TaskToolDetails
1669
+ >;
1670
+
1671
+ /** Source of an agent definition */
1672
+ export type AgentSource = "bundled" | "user" | "project";
1673
+
1674
+ /**
1675
+ * Enforcement policy for a structured subagent output schema.
1676
+ *
1677
+ * `permissive` preserves legacy retry-budget overrides; `strict` turns every
1678
+ * invalid final payload, including an exhausted retry override, into a failed
1679
+ * `schema_violation` result.
1680
+ */
1681
+ export type StructuredSubagentSchemaMode = "permissive" | "strict";
1682
+
1683
+ /** Origin of the schema selected for a structured subagent invocation. */
1684
+ export type StructuredSubagentSchemaSource = "caller" | "agent" | "session" | "none";
1685
+
1686
+ /** Final validation state of a structured subagent invocation. */
1687
+ export type StructuredSubagentValidationStatus = "valid" | "invalid" | "unavailable";
1688
+
1689
+ /**
1690
+ * Parsed structured completion and its schema-validation metadata.
1691
+ *
1692
+ * `data` is present whenever a payload could be assembled or parsed, even when
1693
+ * strict validation rejects it. `error` explains unavailable or invalid
1694
+ * validation without requiring consumers to parse presentation text.
1695
+ */
1696
+ export interface StructuredSubagentOutput {
1697
+ source: StructuredSubagentSchemaSource;
1698
+ mode: StructuredSubagentSchemaMode;
1699
+ status: StructuredSubagentValidationStatus;
1700
+ data?: unknown;
1701
+ error?: string;
1702
+ }
1703
+
1704
+ /** Display cap for a normalized one-line label (roster line, registry `displayName`, prompt field). */
1705
+ export const LABEL_MAX = 80;
1706
+
1707
+ /** Single task item. Fields are optional defensively: args stream in token by token. */
1708
+ export interface TaskItem {
1709
+ /** Stable agent name; becomes the registry/IRC id. Default = generated AdjectiveNoun. */
1710
+ name?: string;
1711
+ /** Agent type to run this item (e.g. "scout"). Defaults to the spawn policy's default agent. */
1712
+ agent?: string;
1713
+ /** The work; required by the schema. */
1714
+ task?: string;
1715
+ /** Per-spawn thinking effort: lowest/middle/highest level the resolved model supports. Overrides the agent's default selector (e.g. `auto`). */
1716
+ effort?: "lo" | "med" | "hi";
1717
+ /** Caller-provided output schema; its presence overrides the selected agent's schema. */
1718
+ outputSchema?: unknown;
1719
+ /** Validation behavior for a caller-provided or inherited output schema. */
1720
+ schemaMode?: "permissive" | "strict";
1721
+ /** Eval-defined tool names exposed to this child. */
1722
+ tools?: string[];
1723
+ /** Run this spawn in an isolated worktree (batch form; flat form carries it top-level). */
1724
+ isolated?: boolean;
1725
+ }
1726
+
1727
+ /**
1728
+ * Runtime params union over both wire shapes. The model sees exactly one shape
1729
+ * (`{ context, tasks[] }` when `task.batch` is on, `{ name?, agent?, task }`
1730
+ * otherwise); runtime stays permissive so internal callers and stale
1731
+ * transcripts using the flat form keep working under either setting.
1732
+ */
1733
+ export interface TaskParams {
1734
+ /** Stable agent name (flat form). */
1735
+ name?: string;
1736
+ /** Agent type to spawn (flat form); omitted values resolve from the session spawn policy. */
1737
+ agent?: string;
1738
+ /** The work (flat form). */
1739
+ task?: string;
1740
+ /** Per-spawn thinking effort (flat form): lowest/middle/highest level the resolved model supports. */
1741
+ effort?: "lo" | "med" | "hi";
1742
+ /** Caller-provided output schema; its presence overrides the selected agent's schema. */
1743
+ outputSchema?: unknown;
1744
+ /** Validation behavior for a caller-provided or inherited output schema. */
1745
+ schemaMode?: "permissive" | "strict";
1746
+ /** Eval-defined tool names exposed to the flat-form child. */
1747
+ tools?: string[];
1748
+ /** Batch form (`task.batch`): one subagent per item. */
1749
+ tasks?: TaskItem[];
1750
+ /** Batch form: shared background prepended to every assignment; required by the batch schema. */
1751
+ context?: string;
1752
+ /** Run in an isolated worktree (flat form; per-item in batch form). */
1753
+ isolated?: boolean;
1754
+ }
1755
+
1756
+ /**
1757
+ * One-line, length-capped label safe for a single roster line, a registry
1758
+ * `displayName`, or a system-prompt field. Collapses every run of whitespace
1759
+ * AND control/format characters — including U+0085 NEL, ESC/ANSI, and the
1760
+ * zero-width separators that `\s` misses — to a single space, then caps length.
1761
+ * So untrusted text (a generated task label, a peer activity gist) can neither
1762
+ * break the line, inject prompt structure, nor smuggle terminal escapes. Caps at
1763
+ * `max` characters (clamped to >= 1; default `LABEL_MAX`), appending an ellipsis when truncated.
1764
+ */
1765
+ export function oneLineLabel(text: string, max = LABEL_MAX): string {
1766
+ const oneLine = text.replace(/[\p{Cc}\p{Cf}\s]+/gu, " ").trim();
1767
+ const cap = Math.max(1, max);
1768
+ // Count/cut by code point, not UTF-16 code unit, so truncation can never
1769
+ // split an astral character into a lone surrogate.
1770
+ const chars = [...oneLine];
1771
+ return chars.length > cap ? `${chars.slice(0, cap - 1).join("")}…` : oneLine;
1772
+ }
1773
+
1774
+ /** A code review finding reported by the reviewer agent */
1775
+ export interface ReviewFinding {
1776
+ title: string;
1777
+ body: string;
1778
+ priority: number;
1779
+ confidence: number;
1780
+ file_path: string;
1781
+ line_start: number;
1782
+ line_end: number;
1783
+ }
1784
+
1785
+ /** Review summary submitted by the reviewer agent */
1786
+ export interface ReviewSummary {
1787
+ overall_correctness: "correct" | "incorrect";
1788
+ explanation: string;
1789
+ confidence: number;
1790
+ }
1791
+
1792
+ /** Structured review data extracted from reviewer agent */
1793
+ export interface ReviewData {
1794
+ findings: ReviewFinding[];
1795
+ summary?: ReviewSummary;
1796
+ }
1797
+
1798
+ /** Details extracted from a subagent `yield` tool call for final-result assembly and task rendering. */
1799
+ export interface YieldItem {
1800
+ data?: unknown;
1801
+ status?: "success" | "aborted";
1802
+ error?: string;
1803
+ /** A string label is terminal; a non-empty array of labels is incremental. */
1804
+ type?: string | string[];
1805
+ /** Resolve this yield's payload from the latest durable assistant text instead of `data`. */
1806
+ useLastTurn?: boolean;
1807
+ /** True when an incremental workpool yield completed every item in its batch. */
1808
+ complete?: boolean;
1809
+ /**
1810
+ * Set by the in-tool yield validator when it exhausted its retry budget and
1811
+ * accepted schema-invalid data anyway. The executor preserves that override
1812
+ * during post-mortem validation.
1813
+ */
1814
+ schemaOverridden?: boolean;
1815
+ }
1816
+
1817
+ /** Progress tracking for a single agent */
1818
+ export interface AgentProgress {
1819
+ index: number;
1820
+ id: string;
1821
+ agent: string;
1822
+ agentSource: AgentSource;
1823
+ status: "pending" | "running" | "completed" | "failed" | "aborted";
1824
+ task: string;
1825
+ assignment?: string;
1826
+ description?: string;
1827
+ lastIntent?: string;
1828
+ currentTool?: string;
1829
+ currentToolArgs?: string;
1830
+ currentToolStartMs?: number;
1831
+ recentTools: Array<{ tool: string; args: string; endMs: number }>;
1832
+ recentOutput: string[];
1833
+ toolCount: number;
1834
+ /** Count of assistant requests (assistant message_end events) across the run. Drives the soft request budget guard. */
1835
+ requests: number;
1836
+ /** Cumulative input + output + cacheWrite tokens across all turns. Excludes cacheRead (re-reads cached context every turn, making cumulative sum misleading). */
1837
+ tokens: number;
1838
+ /**
1839
+ * Current per-turn context size: latest assistant message's `usage.totalTokens`.
1840
+ * This is the number to compare against `contextWindow` — what compaction
1841
+ * decides on, what the user typically reads as "how full is the context".
1842
+ * Distinct from `tokens`, which is a lifetime billing-volume counter.
1843
+ */
1844
+ contextTokens?: number;
1845
+ /** Model's context window in tokens, when known. Lets the UI render `<curr>/<window>` gauges. */
1846
+ contextWindow?: number;
1847
+ /** Cumulative billing cost in USD, accumulated incrementally from message_end events. */
1848
+ cost: number;
1849
+ durationMs: number;
1850
+ modelOverride?: string | string[];
1851
+ /** Explicit pre-expansion model role alias selected for this run. */
1852
+ modelRole?: string;
1853
+ /** Resolved model display string in the form `<provider>/<id>`, optionally suffixed with `:<thinkingLevel>` when the level was set explicitly. Undefined when the model could not be resolved. */
1854
+ resolvedModel?: string;
1855
+ /** Provider/id including routing, with no added thinking suffix. */
1856
+ resolvedModelIdentity?: string;
1857
+ /** Explicit thinking metadata; never inferred from the model identity. */
1858
+ resolvedThinkingLevel?: ConfiguredThinkingLevel;
1859
+ /** True when {@link resolvedModel} is the target of an active retry fallback (not the originally configured model). Lets observer-only UIs (collab guests, Agent Hub rows with no live session) flag the fallback and keep the provider. */
1860
+ resolvedModelIsFallback?: boolean;
1861
+ /** True when a live advisor was attached to this run's session, not merely enabled in settings. */
1862
+ advisor?: boolean;
1863
+ /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
1864
+ extractedToolData?: Record<string, unknown[]>;
1865
+ /**
1866
+ * Auto-retry state when the subagent is sleeping between provider retries
1867
+ * (e.g. 429 rate-limit with retry-after). Cleared when the retry resolves
1868
+ * or fails. Surfacing this to the parent prevents the task tool from
1869
+ * looking indefinitely "in progress" when a child is actually blocked on
1870
+ * provider quota.
1871
+ */
1872
+ retryState?: {
1873
+ attempt: number;
1874
+ maxAttempts: number;
1875
+ delayMs: number;
1876
+ errorMessage: string;
1877
+ startedAtMs: number;
1878
+ };
1879
+ /**
1880
+ * Terminal retry failure surfaced once the subagent gave up retrying
1881
+ * (e.g. retry-after exceeded the cap, or all attempts exhausted). Carries
1882
+ * the final error so the parent UI can render "blocked: rate-limited"
1883
+ * instead of waiting for a status that never arrives.
1884
+ */
1885
+ retryFailure?: {
1886
+ attempt: number;
1887
+ errorMessage: string;
1888
+ };
1889
+ /**
1890
+ * Snapshot of the most recent `task` tool call's in-flight `TaskToolDetails`,
1891
+ * captured from `tool_execution_update`. Lets the parent UI surface live
1892
+ * nested-subagent progress while this agent is still inside its own `task`
1893
+ * call. Cleared when the call ends — finalized data lives in
1894
+ * `extractedToolData.task` after that.
1895
+ */
1896
+ inflightTaskDetails?: TaskToolDetails;
1897
+ }
1898
+
1899
+ /** Result from a single agent execution */
1900
+ export interface SingleResult {
1901
+ index: number;
1902
+ id: string;
1903
+ agent: string;
1904
+ agentSource: AgentSource;
1905
+ task: string;
1906
+ assignment?: string;
1907
+ description?: string;
1908
+ lastIntent?: string;
1909
+ exitCode: number;
1910
+ output: string;
1911
+ stderr: string;
1912
+ truncated: boolean;
1913
+ /**
1914
+ * Parsed structured completion and validation metadata, when this invocation
1915
+ * selected an output schema or strict schema mode.
1916
+ */
1917
+ structuredOutput?: StructuredSubagentOutput;
1918
+ durationMs: number;
1919
+ /** Cumulative input + output + cacheWrite tokens across all turns. Excludes cacheRead (re-reads cached context every turn, making cumulative sum misleading). */
1920
+ tokens: number;
1921
+ /** Count of assistant requests (assistant message_end events) across the run. */
1922
+ requests: number;
1923
+ /** Latest per-turn context size at task completion. See `AgentProgress.contextTokens`. */
1924
+ contextTokens?: number;
1925
+ /** Model's context window in tokens, when known. */
1926
+ contextWindow?: number;
1927
+ modelOverride?: string | string[];
1928
+ /** Explicit pre-expansion model role alias selected for this run. */
1929
+ modelRole?: string;
1930
+ /** Resolved model display string in the form `<provider>/<id>`, optionally suffixed with `:<thinkingLevel>` when the level was set explicitly. Omitted from tool-result JSON when undefined to keep wire payloads small. */
1931
+ resolvedModel?: string;
1932
+ /** Retains the unambiguous identity from {@link AgentProgress.resolvedModelIdentity}. */
1933
+ resolvedModelIdentity?: string;
1934
+ /** Retains {@link AgentProgress.resolvedThinkingLevel} after settlement. */
1935
+ resolvedThinkingLevel?: ConfiguredThinkingLevel;
1936
+ /** True when {@link resolvedModel} is the target of an active retry fallback. Mirrors {@link AgentProgress.resolvedModelIsFallback} onto the settled result. */
1937
+ resolvedModelIsFallback?: boolean;
1938
+ /** Retains {@link AgentProgress.advisor} after the advised session is disposed. */
1939
+ advisor?: boolean;
1940
+ error?: string;
1941
+ aborted?: boolean;
1942
+ abortReason?: string;
1943
+ /** Aggregated usage from the subprocess, accumulated incrementally from message_end events. */
1944
+ usage?: Usage;
1945
+ /** Output path for the task result */
1946
+ outputPath?: string;
1947
+ /**
1948
+ * Ran inside an isolation worktree. Such agents are parked without a
1949
+ * reviver once the worktree is torn down, so they are never resumable or
1950
+ * messageable after the run — summaries must not suggest otherwise.
1951
+ */
1952
+ isolated?: boolean;
1953
+ /** Patch path for isolated worktree output */
1954
+ patchPath?: string;
1955
+ /**
1956
+ * Whether `patchPath` holds a non-empty root-repo diff. `false` when the
1957
+ * agent's changes all live in nested repos (see `nestedPatchPaths`), so
1958
+ * summaries do not claim the root patch captured anything.
1959
+ */
1960
+ hasRootChanges?: boolean;
1961
+ /** Branch name for isolated branch-mode output */
1962
+ branchName?: string;
1963
+ /**
1964
+ * Baseline commit SHA the task branch was created from. Passed to
1965
+ * `mergeTaskBranches` so cherry-pick uses the inclusive range
1966
+ * `branchBaseSha..branchName` and preserves every agent commit's message.
1967
+ */
1968
+ branchBaseSha?: string;
1969
+ /** Nested repo patches to apply after parent merge */
1970
+ nestedPatches?: NestedRepoPatch[];
1971
+ /**
1972
+ * On-disk copies of `nestedPatches`, one file per nested repo, written
1973
+ * before the isolation workspace is torn down. The workspace is the only
1974
+ * other copy of that work, so these paths are the durable record.
1975
+ */
1976
+ nestedPatchPaths?: string[];
1977
+ /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
1978
+ extractedToolData?: Record<string, unknown[]>;
1979
+ /**
1980
+ * Terminal retry failure, when the subagent exited because the auto-retry
1981
+ * loop gave up (retry-after exceeded the cap, or all attempts exhausted).
1982
+ * Lets the parent task tool surface a "blocked: rate-limited" outcome
1983
+ * instead of a generic failure.
1984
+ */
1985
+ retryFailure?: {
1986
+ attempt: number;
1987
+ errorMessage: string;
1988
+ };
1989
+ /** Output metadata for agent:// URL integration */
1990
+ outputMeta?: { lineCount: number; charCount: number };
1991
+ }
1992
+
1993
+ /** Tool details for TUI rendering */
1994
+ export interface TaskToolDetails {
1995
+ projectAgentsDir: string | null;
1996
+ results: SingleResult[];
1997
+ totalDurationMs: number;
1998
+ /** Aggregated usage across all subagents. */
1999
+ usage?: Usage;
2000
+ outputPaths?: string[];
2001
+ progress?: AgentProgress[];
2002
+ async?: {
2003
+ state: "running" | "completed" | "failed";
2004
+ jobId: string;
2005
+ type: "task";
2006
+ };
2007
+ }
2008
+
2009
+ /** Patch and baseline metadata for an isolated nested repository. */
2010
+ export interface NestedRepoPatch {
2011
+ relativePath: string;
2012
+ patch: string;
2013
+ }
2014
+
2015
+ /** Severity level of a review finding. */
2016
+ export type FindingPriority = "P0" | "P1" | "P2" | "P3";
2017
+
2018
+ /** Severity ordering, glyph, and color for a review finding. */
2019
+ export interface FindingPriorityInfo {
2020
+ ord: 0 | 1 | 2 | 3;
2021
+ symbol: "status.error" | "status.warning" | "status.info";
2022
+ color: ThemeColor;
2023
+ }
2024
+
2025
+ const PRIORITY_INFO: Record<FindingPriority, FindingPriorityInfo> = {
2026
+ P0: { ord: 0, symbol: "status.error", color: "error" },
2027
+ P1: { ord: 1, symbol: "status.warning", color: "warning" },
2028
+ P2: { ord: 2, symbol: "status.warning", color: "muted" },
2029
+ P3: { ord: 3, symbol: "status.info", color: "accent" },
2030
+ };
2031
+
2032
+ /** Review severity levels in descending priority. */
2033
+ export const PRIORITY_LABELS: FindingPriority[] = ["P0", "P1", "P2", "P3"];
2034
+
2035
+ /** Tests whether a value is a supported review severity. */
2036
+ export function isFindingPriority(value: unknown): value is FindingPriority {
2037
+ return value === "P0" || value === "P1" || value === "P2" || value === "P3";
2038
+ }
2039
+
2040
+ /** Returns display metadata for a review severity. */
2041
+ export function getPriorityInfo(priority: FindingPriority): FindingPriorityInfo {
2042
+ return PRIORITY_INFO[priority] ?? { ord: 3, symbol: "status.info", color: "muted" };
2043
+ }
2044
+ /** Validated source location and content of a review finding. */
2045
+ export interface FindingDetails {
2046
+ title: string;
2047
+ body: string;
2048
+ priority: FindingPriority;
2049
+ confidence: number;
2050
+ file_path: string;
2051
+ line_start: number;
2052
+ line_end: number;
2053
+ }
2054
+
2055
+ function normalizeFindingPriority(value: unknown): FindingPriority | undefined {
2056
+ if (isFindingPriority(value)) return value;
2057
+ if (value === 0) return "P0";
2058
+ if (value === 1) return "P1";
2059
+ if (value === 2) return "P2";
2060
+ if (value === 3) return "P3";
2061
+ return undefined;
2062
+ }
2063
+
2064
+ /** Validates and normalizes a persisted review finding. */
2065
+ export function parseFindingDetails(value: unknown): FindingDetails | undefined {
2066
+ if (!isRecord(value)) return undefined;
2067
+
2068
+ const title = typeof value.title === "string" ? value.title : undefined;
2069
+ const body = typeof value.body === "string" ? value.body : undefined;
2070
+ const priority = normalizeFindingPriority(value.priority);
2071
+ const confidence =
2072
+ typeof value.confidence === "number" &&
2073
+ Number.isFinite(value.confidence) &&
2074
+ value.confidence >= 0 &&
2075
+ value.confidence <= 1
2076
+ ? value.confidence
2077
+ : undefined;
2078
+ const filePath = typeof value.file_path === "string" && value.file_path.length > 0 ? value.file_path : undefined;
2079
+ const lineStart =
2080
+ typeof value.line_start === "number" && Number.isFinite(value.line_start) ? value.line_start : undefined;
2081
+ const lineEnd = typeof value.line_end === "number" && Number.isFinite(value.line_end) ? value.line_end : undefined;
2082
+
2083
+ if (
2084
+ title === undefined ||
2085
+ body === undefined ||
2086
+ priority === undefined ||
2087
+ confidence === undefined ||
2088
+ filePath === undefined ||
2089
+ lineStart === undefined ||
2090
+ lineEnd === undefined
2091
+ ) {
2092
+ return undefined;
2093
+ }
2094
+
2095
+ return {
2096
+ title,
2097
+ body,
2098
+ priority,
2099
+ confidence,
2100
+ file_path: filePath,
2101
+ line_start: lineStart,
2102
+ line_end: lineEnd,
2103
+ };
2104
+ }
2105
+ /** SubmitReviewDetails - used for rendering review results from yield tool */
2106
+ export interface SubmitReviewDetails {
2107
+ overall_correctness: "correct" | "incorrect";
2108
+ explanation: string;
2109
+ confidence: number;
2110
+ }