@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,1395 @@
1
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
+ import { Box } from "../components/box";
3
+ import { SPINNER_ADVANCE_MS } from "../components/loader";
4
+ import { Image } from "../components/image";
5
+ import { Spacer } from "../components/spacer";
6
+ import { Text } from "../components/text";
7
+ import { getImageDimensions, ImageProtocol, imageFallback, TERMINAL } from "../terminal-capabilities";
8
+ import { type Component, Container, type TUI } from "../tui";
9
+ import { truncateToWidth } from "../utils";
10
+ import { getProjectDir, isRecord, logger, sanitizeText } from "@oh-my-pi/pi-utils";
11
+ import type { Theme } from "../theme/theme";
12
+ import { ensureThemeSync, getThemeEpoch, theme } from "../theme/theme";
13
+ import {
14
+ type FirstResultViewportRepaint,
15
+ type ToolActivitySummary,
16
+ type ToolRenderer,
17
+ toolRenderers,
18
+ } from "../tools/index";
19
+ import { BASH_DEFAULT_PREVIEW_LINES } from "../tools/bash";
20
+ import { formatDefaultToolExecution } from "../tools/default-renderer";
21
+ import { type EditMode, type PerFileDiffPreview, renderStreamingFallback } from "../tools/edit";
22
+ import { EVAL_DEFAULT_PREVIEW_LINES } from "../tools/eval";
23
+ import { taskCardAgentIds } from "../tools/task";
24
+ import { TODO_STRIKE_TOTAL_FRAMES, type TodoToolDetails } from "../tools/todo";
25
+ import { isWaitingPollDetails } from "../tools/hub";
26
+ import { formatStatusIcon, replaceTabs, resolveImageOptions } from "../render/render-utils";
27
+ import type { XdevMountedState } from "../tools/xdev";
28
+ import { isFramedBlockComponent, markFramedBlockComponent, renderStatusLine, WidthAwareText } from "../render/index";
29
+ import { convertImageToPng } from "./image-loading";
30
+ import { sanitizeWithOptionalSixelPassthrough } from "../render/sixel";
31
+ import { renderDiff } from "../chrome/diff";
32
+ import { type AnimationFrame, trimBlankEdges } from "../chrome/transcript-container";
33
+
34
+ /** Resolves the canonical renderer key while retaining the provider's wire name in message history. */
35
+ export function toolRenderName(wireName: string, tool: AgentTool | undefined): string {
36
+ return tool?.name ?? wireName;
37
+ }
38
+ type DisplaceableToolName = "hub" | "todo";
39
+
40
+ function isTodoToolDetails(details: unknown): details is TodoToolDetails {
41
+ return (
42
+ typeof details === "object" &&
43
+ details !== null &&
44
+ "phases" in details &&
45
+ Array.isArray((details as { phases?: unknown }).phases)
46
+ );
47
+ }
48
+
49
+ interface ToolImageBlock {
50
+ data?: string;
51
+ mimeType?: string;
52
+ }
53
+
54
+ function imageBlocksFromDetails(details: unknown): ToolImageBlock[] {
55
+ if (!isRecord(details) || !Array.isArray(details.images)) return [];
56
+ return details.images.filter(
57
+ (image): image is ToolImageBlock =>
58
+ isRecord(image) &&
59
+ (image.data === undefined || typeof image.data === "string") &&
60
+ (image.mimeType === undefined || typeof image.mimeType === "string"),
61
+ );
62
+ }
63
+
64
+ function displaceableToolName(
65
+ toolName: string,
66
+ result: { details?: unknown; isError?: boolean },
67
+ isPartial: boolean,
68
+ ): DisplaceableToolName | undefined {
69
+ if (result.isError === true) return undefined;
70
+ if (toolName === "hub" && isWaitingPollDetails(result.details)) return "hub";
71
+ if (toolName === "todo" && !isPartial && isTodoToolDetails(result.details)) return "todo";
72
+ return undefined;
73
+ }
74
+
75
+ function isEditLikeToolName(toolName: string): boolean {
76
+ return toolName === "edit" || toolName === "apply_patch";
77
+ }
78
+
79
+ function resolveEditModeForTool(toolName: string, tool: AgentTool | undefined): EditMode | undefined {
80
+ if (toolName === "apply_patch") return "apply_patch";
81
+ if (toolName !== "edit") return undefined;
82
+ return (tool as { mode?: EditMode } | undefined)?.mode;
83
+ }
84
+
85
+ type ToolRendererStage = "call" | "result";
86
+
87
+ class SafeToolRendererComponent implements Component {
88
+ #toolName: string;
89
+ #stage: ToolRendererStage;
90
+ #component: Component;
91
+ #fallback: () => Component | undefined;
92
+ #warned = false;
93
+ readonly wantsKeyRelease: boolean | undefined;
94
+
95
+ constructor(
96
+ toolName: string,
97
+ stage: ToolRendererStage,
98
+ component: Component,
99
+ fallback: () => Component | undefined,
100
+ ) {
101
+ this.#toolName = toolName;
102
+ this.#stage = stage;
103
+ this.#component = component;
104
+ this.#fallback = fallback;
105
+ this.wantsKeyRelease = component.wantsKeyRelease;
106
+ if (isFramedBlockComponent(component)) {
107
+ markFramedBlockComponent(this);
108
+ }
109
+ }
110
+
111
+ render(width: number): readonly string[] {
112
+ try {
113
+ return this.#component.render(width);
114
+ } catch (err) {
115
+ if (!this.#warned) {
116
+ this.#warned = true;
117
+ logger.warn("Tool renderer failed", { tool: this.#toolName, stage: this.#stage, error: String(err) });
118
+ }
119
+ return this.#fallback()?.render(width) ?? [];
120
+ }
121
+ }
122
+
123
+ handleInput(data: string): void {
124
+ const handleInput = this.#component.handleInput;
125
+ if (handleInput === undefined) return;
126
+ handleInput.call(this.#component, data);
127
+ }
128
+
129
+ invalidate(): void {
130
+ const invalidate = this.#component.invalidate;
131
+ if (invalidate === undefined) return;
132
+ invalidate.call(this.#component);
133
+ }
134
+
135
+ setIgnoreTight(ignore: boolean): void {
136
+ const setIgnoreTight = this.#component.setIgnoreTight;
137
+ if (setIgnoreTight === undefined) return;
138
+ setIgnoreTight.call(this.#component, ignore);
139
+ }
140
+
141
+ dispose(): void {
142
+ const dispose = this.#component.dispose;
143
+ if (dispose === undefined) return;
144
+ dispose.call(this.#component);
145
+ }
146
+ }
147
+
148
+ /** Minimal TUI surface ToolExecutionComponent uses to schedule repaints and share image budget. */
149
+ export interface ToolExecutionUi {
150
+ requestRender(): void;
151
+ requestComponentRender(component: Component): void;
152
+ resetDisplay(): void;
153
+ imageBudget?: TUI["imageBudget"];
154
+ }
155
+
156
+ export interface ToolExecutionOptions {
157
+ showImages?: boolean; // default: true (only used if terminal supports images)
158
+ /** Allow the name-keyed renderer registry only when the active tool is the built-in implementation. */
159
+ useBuiltInRenderer?: boolean;
160
+ }
161
+
162
+ export interface ToolExecutionHandle extends Component {
163
+ updateArgs(args: unknown, toolCallId?: string): void;
164
+ updateStreamPreview?(update: unknown): void;
165
+ updateResult(
166
+ result: {
167
+ content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
168
+ details?: unknown;
169
+ isError?: boolean;
170
+ },
171
+ isPartial?: boolean,
172
+ toolCallId?: string,
173
+ ): void;
174
+ setArgsComplete(toolCallId?: string): void;
175
+ setExecutionStarted(toolCallId?: string): void;
176
+ setExpanded(expanded: boolean): void;
177
+ setToolActivityVisible(visible: boolean): void;
178
+ /** Mark the call parked: it returned, but stays tracked for async job frames. */
179
+ parkAsBackground(): void;
180
+ /** Seal the block as final history and stop its animations. */
181
+ seal(): void;
182
+ }
183
+
184
+ /** Phase-locked spinner glyph index shared by every live tool block so parallel
185
+ * spinners advance in lockstep instead of each tracking its own start time. */
186
+ export function sharedSpinnerFrame(frameCount: number, now: number = performance.now()): number {
187
+ return frameCount > 0 ? Math.floor(now / SPINNER_ADVANCE_MS) % frameCount : 0;
188
+ }
189
+
190
+ /** Live tool blocks currently driving a spinner. A single shared ticker (below)
191
+ * advances and repaints every registered block per glyph step, so N concurrent
192
+ * live/streaming blocks — e.g. parallel `task` subagents — cost one 80ms timer
193
+ * and one coalesced render frame per tick instead of N unsynchronized timers
194
+ * each independently waking the render scheduler (issue #8731). */
195
+ const liveSpinnerBlocks = new Set<ToolExecutionComponent>();
196
+ let sharedSpinnerTimer: NodeJS.Timeout | undefined;
197
+
198
+ /** Arm the shared spinner ticker if it is not already running. */
199
+ function ensureSharedSpinnerTicker(): void {
200
+ if (sharedSpinnerTimer) return;
201
+ sharedSpinnerTimer = setInterval(() => {
202
+ const frame = sharedSpinnerFrame(theme.spinnerFrames.length);
203
+ // Removing the current block mid-iteration is safe on a Set.
204
+ for (const block of liveSpinnerBlocks) block.tickSpinner(frame);
205
+ }, SPINNER_ADVANCE_MS);
206
+ }
207
+
208
+ /** Register a live block with the shared ticker, starting it on first use. */
209
+ function registerSpinnerBlock(block: ToolExecutionComponent): void {
210
+ liveSpinnerBlocks.add(block);
211
+ ensureSharedSpinnerTicker();
212
+ }
213
+
214
+ /** Drop a block; stop the ticker once no live block remains. */
215
+ function unregisterSpinnerBlock(block: ToolExecutionComponent): void {
216
+ if (!liveSpinnerBlocks.delete(block)) return;
217
+ if (liveSpinnerBlocks.size === 0 && sharedSpinnerTimer) {
218
+ clearInterval(sharedSpinnerTimer);
219
+ sharedSpinnerTimer = undefined;
220
+ }
221
+ }
222
+
223
+ /** Stop the shared spinner ticker and drop every registered live block.
224
+ * Called on interactive-mode teardown so a stray live block cannot keep the
225
+ * process-wide 80ms interval alive past shutdown (lingering event-loop
226
+ * handles pin the process; cf. `postmortem.quit`). Test files that assert on
227
+ * ticker arming also use this to start from a clean slate. */
228
+ export function stopSharedSpinnerTicker(): void {
229
+ liveSpinnerBlocks.clear();
230
+ if (sharedSpinnerTimer) {
231
+ clearInterval(sharedSpinnerTimer);
232
+ sharedSpinnerTimer = undefined;
233
+ }
234
+ }
235
+
236
+ // Stable per-instance counter so each tool execution's inline images get a
237
+ // graphics id that survives child re-creation (the image budget keys off it).
238
+ let toolExecutionInstanceSeq = 0;
239
+
240
+ /**
241
+ * Component that renders a tool call with its result (updateable)
242
+ */
243
+ export class ToolExecutionComponent extends Container {
244
+ #contentBox: Box; // Used for custom tools and bash visual truncation
245
+ #contentText: WidthAwareText; // Generic fallback (no custom/built-in renderer)
246
+ // Which container the constructor mounted: bespoke/built-in renderers use
247
+ // #contentBox, everything else the generic #contentText fallback.
248
+ #usesContentBox = false;
249
+ #multiFileBoxes: (Box | Spacer)[] = []; // Extra boxes for multi-file edit results
250
+ #imageComponents: Image[] = [];
251
+ #imageSpacers: Spacer[] = [];
252
+ readonly #instanceId = ++toolExecutionInstanceSeq;
253
+ #toolName: string;
254
+ #toolLabel: string;
255
+ #args: unknown;
256
+ #expanded = false;
257
+ #allocation = Number.POSITIVE_INFINITY;
258
+ #presentationFrame: AnimationFrame = { tick: 0, now: 0 };
259
+ #toolActivityVisible = true;
260
+ #showImages: boolean;
261
+ #isPartial = true;
262
+ // A background task whose call already returned; later async job frames are
263
+ // partial updates, but the block is ready to retire as history.
264
+ #parkedBackground = false;
265
+ #resultVersion = 0;
266
+ // Post-finalize mutation counter (see FinalizableBlock.getTranscriptBlockVersion):
267
+ // a tool block can keep changing after isTranscriptBlockFinalized() first
268
+ // returns true — an async task's terminal result settlement, seal(), or an
269
+ // expansion toggle — and the transcript's width-epoch resolution and
270
+ // committed-render bypass must observe those mutations.
271
+ #blockVersion = 0;
272
+ #lastDisplayKey: string | undefined;
273
+ // Bumped whenever a render input that #rebuildDisplay consumes but the memo
274
+ // key cannot cheaply hash changes: streamed call args, native edit-diff
275
+ // previews, and Kitty PNG conversions. Folded into the dirty key so those
276
+ // updates are not swallowed by the memo (see #updateDisplay).
277
+ #displayInputVersion = 0;
278
+ // Set once #rebuildDisplay has populated the display. Replaces a
279
+ // #contentBox.children.length probe so the memo fast-path also covers the
280
+ // #contentText fallback path (which leaves #contentBox empty).
281
+ #displayBuilt = false;
282
+ // Number of Image children the last rebuild emitted. Only when this is > 0 does
283
+ // the memo key fold in viewport-dependent image sizing (resolveImageOptions),
284
+ // so a terminal resize re-shapes image-bearing results to rescale them without
285
+ // forcing the common image-free result to re-shape on every resize tick.
286
+ #renderedImageCount = 0;
287
+ #tool?: AgentTool;
288
+ #renderer?: ToolRenderer;
289
+ #ui: ToolExecutionUi;
290
+ #result?: {
291
+ content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
292
+ isError?: boolean;
293
+ details?: unknown;
294
+ };
295
+ // Edit preview state
296
+ #editMode?: EditMode;
297
+ #editDiffPreview?: PerFileDiffPreview[];
298
+ #previewReady?: PromiseWithResolvers<void>;
299
+ // Cached converted images for Kitty protocol (which requires PNG), keyed by index
300
+ #convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
301
+ // Spinner animation for partial task results
302
+ #spinnerFrame?: number;
303
+ #spinnerActive = false;
304
+ // Todo write completion strikethrough reveal animation
305
+ #todoStrikeInterval?: NodeJS.Timeout;
306
+ // Track if args are still being streamed (for edit/write spinner)
307
+ #argsComplete = false;
308
+ #executionStarted = false;
309
+ // Sealed once the tool reaches a terminal state (result delivered, or the
310
+ // turn abandoned it without one). Until then the block remains active so a
311
+ // late result can update its streaming preview.
312
+ #sealed = false;
313
+ // Tool result snapshots that may be superseded by a later same-tool call
314
+ // while still in the mutable viewport. `hub` uses this for repeated all-running polls; `todo` uses
315
+ // it for per-turn state snapshots so only the latest list remains visible.
316
+ #displaceableByToolName: DisplaceableToolName | undefined;
317
+ // Execution start on the presentation clock (performance.now domain, the
318
+ // same domain as AnimationFrame.now supplied by the transcript allocator).
319
+ #executionStartedAtNow: number | undefined;
320
+ // Wall clock captured whenever a task card is rebuilt.
321
+ #taskRenderNowMs = Date.now();
322
+ // Set on each `render()` when the last painted pending shape must be
323
+ // replayed wholesale when the first result arrives. Reset gates key off
324
+ // these so a topology-changing update that lands before the shape reaches
325
+ // the terminal never triggers an unnecessary full-viewport replay.
326
+ #firstResultViewportRepaintShapePainted = false;
327
+ #partialResultShapePainted = false;
328
+ #renderState: {
329
+ spinnerFrame?: number;
330
+ expanded: boolean;
331
+ isPartial: boolean;
332
+ argsComplete?: boolean;
333
+ executionStarted?: boolean;
334
+ renderContext?: Record<string, unknown>;
335
+ } = {
336
+ expanded: false,
337
+ isPartial: true,
338
+ argsComplete: false,
339
+ executionStarted: false,
340
+ };
341
+
342
+ constructor(
343
+ toolName: string,
344
+ args: unknown,
345
+ options: ToolExecutionOptions = {},
346
+ tool: AgentTool | undefined,
347
+ ui: ToolExecutionUi,
348
+ _cwd: string = getProjectDir(),
349
+ _toolCallId?: string,
350
+ ) {
351
+ super();
352
+ ensureThemeSync();
353
+ this.#toolName = toolName;
354
+ this.#toolLabel = tool?.label ?? toolName;
355
+ this.#renderer = options.useBuiltInRenderer === false ? undefined : toolRenderers[toolName];
356
+ this.#showImages = options.showImages ?? true;
357
+ this.#tool = tool;
358
+ this.#ui = ui;
359
+ this.#args = args;
360
+ this.#editMode = resolveEditModeForTool(toolName, tool);
361
+ if (this.#editMode) this.#previewReady = Promise.withResolvers<void>();
362
+
363
+ // Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins.
364
+ // paddingY is 1 so background-tinted blocks (custom/extension tools and the
365
+ // generic fallback) get top/bottom breathing room. TranscriptContainer
366
+ // strips PLAIN-blank edges, so framed/minimal blocks (no bg set) drop these
367
+ // lines and keep their tight spacing — only tinted lines survive.
368
+ this.#contentBox = new Box(0, 1);
369
+ this.#contentText = new WidthAwareText(contentWidth => this.#renderDefaultCard(contentWidth), 1, 1);
370
+
371
+ // Use Box for custom tools or built-in tools with rich renderers.
372
+ const hasCustomRenderer = !!(tool?.renderCall || tool?.renderResult);
373
+ this.#usesContentBox = hasCustomRenderer || this.#renderer !== undefined;
374
+ if (this.#usesContentBox) {
375
+ this.addChild(this.#contentBox);
376
+ } else {
377
+ this.addChild(this.#contentText);
378
+ }
379
+ // Tool blocks are visually distinct cards (background-tinted or framed),
380
+ // so keep their horizontal padding even when the user enables tight layout.
381
+ this.setIgnoreTight(true);
382
+
383
+ this.#updateSpinnerAnimation();
384
+ this.#updateDisplay();
385
+ }
386
+
387
+ updateArgs(args: unknown, _toolCallId?: string): void {
388
+ // Reference-equality short-circuit before any further work. Callers
389
+ // always allocate a new arg object on each streamed delta (see
390
+ // event-controller.ts and ui-helpers.ts), so a same-reference assignment
391
+ // signals "nothing meaningful changed" and the renderer can skip.
392
+ if (args === this.#args) return;
393
+ this.#args = args;
394
+ this.#displayInputVersion++;
395
+ this.#updateSpinnerAnimation();
396
+ this.#updateDisplay();
397
+ }
398
+
399
+ /**
400
+ * Signal that args are complete (tool is about to execute).
401
+ * This triggers an immediate final diff computation for edit-like tools.
402
+ */
403
+ setArgsComplete(_toolCallId?: string): void {
404
+ const alreadyComplete = this.#argsComplete;
405
+ this.#argsComplete = true;
406
+ this.#updateSpinnerAnimation();
407
+ if (alreadyComplete) return;
408
+ this.#displayInputVersion++;
409
+ this.#updateDisplay();
410
+ }
411
+
412
+ /**
413
+ * Signal that this specific call has begun executing (`tool_execution_start`).
414
+ * Distinct from {@link setArgsComplete}: exclusive writes are marked complete
415
+ * at `message_end` but stay queued until this fires for that call.
416
+ */
417
+ setExecutionStarted(_toolCallId?: string): void {
418
+ if (this.#executionStarted) return;
419
+ this.#executionStarted = true;
420
+ this.#executionStartedAtNow = performance.now();
421
+ this.#argsComplete = true;
422
+ this.#updateSpinnerAnimation();
423
+ this.#displayInputVersion++;
424
+ this.#updateDisplay();
425
+ }
426
+
427
+ /**
428
+ * Resolve once the final (`streaming: false`) preview batch lands or the
429
+ * result settles. Callers without a live batch source (gallery snapshots)
430
+ * pass a budget so a component that will never receive a batch cannot
431
+ * hang them; live approval gates omit it and wait indefinitely.
432
+ */
433
+ async whenPreviewSettled(timeoutMs?: number): Promise<void> {
434
+ if (this.#previewReady === undefined) return;
435
+ if (timeoutMs === undefined) {
436
+ await this.#previewReady.promise;
437
+ return;
438
+ }
439
+ await Promise.race([this.#previewReady.promise, Bun.sleep(timeoutMs)]);
440
+ }
441
+
442
+ updateStreamPreview(update: unknown): void {
443
+ if (
444
+ this.#previewDiffSettled() ||
445
+ update === null ||
446
+ typeof update !== "object" ||
447
+ !("files" in update) ||
448
+ !Array.isArray(update.files)
449
+ ) {
450
+ return;
451
+ }
452
+ if ("streaming" in update && update.streaming === false) this.#previewReady?.resolve();
453
+ if (update.files.length === 0) return;
454
+ const rawFiles: unknown[] = update.files;
455
+ const files = rawFiles.filter(
456
+ (
457
+ file,
458
+ ): file is {
459
+ path: string;
460
+ diff?: string | null;
461
+ firstChangedLine?: number | null;
462
+ error?: string | null;
463
+ } => file !== null && typeof file === "object" && "path" in file && typeof file.path === "string",
464
+ );
465
+ if (files.length === 0) return;
466
+ this.#editDiffPreview = files.map(file => ({
467
+ path: file.path,
468
+ diff: file.diff ?? undefined,
469
+ firstChangedLine: file.firstChangedLine ?? undefined,
470
+ error: file.error ?? undefined,
471
+ }));
472
+ this.#displayInputVersion++;
473
+ this.#updateDisplay();
474
+ this.#ui.requestRender();
475
+ }
476
+
477
+ /**
478
+ * True once a terminal result makes the streaming preview moot: renderResult
479
+ * prefers `details.diff` and renders errors from the result text, consulting
480
+ * the computed preview only for a non-error result that carries no details.
481
+ */
482
+ #previewDiffSettled(): boolean {
483
+ const result = this.#result;
484
+ return result !== undefined && !this.#isPartial && (result.isError === true || result.details != null);
485
+ }
486
+
487
+ updateResult(
488
+ result: {
489
+ content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
490
+ details?: unknown;
491
+ isError?: boolean;
492
+ },
493
+ isPartial = false,
494
+ _toolCallId?: string,
495
+ ): void {
496
+ const hadNoResult = this.#result === undefined;
497
+ const wasPartialResult = this.#result !== undefined && this.#isPartial;
498
+ const firstResultRepaintShapePainted = this.#firstResultViewportRepaintShapePainted;
499
+ const partialResultPainted = this.#partialResultShapePainted;
500
+ this.#firstResultViewportRepaintShapePainted = false;
501
+ this.#partialResultShapePainted = false;
502
+ this.#result = result;
503
+ this.#resultVersion++;
504
+ this.#blockVersion++;
505
+ this.#isPartial = isPartial;
506
+ this.#displaceableByToolName = displaceableToolName(this.#toolName, result, isPartial);
507
+ // When tool is complete, ensure args are marked complete so spinner stops
508
+ if (!isPartial) {
509
+ this.#argsComplete = true;
510
+ this.#previewReady?.resolve();
511
+ }
512
+ this.#updateSpinnerAnimation();
513
+ this.#updateTodoStrikeAnimation();
514
+ this.#updateDisplay();
515
+ this.#resetDisplayForResultTopologyChange(
516
+ hadNoResult && firstResultRepaintShapePainted,
517
+ wasPartialResult && partialResultPainted,
518
+ isPartial,
519
+ );
520
+ // Convert non-PNG images to PNG for Kitty protocol (async)
521
+ this.#maybeConvertImagesForKitty();
522
+ }
523
+
524
+ /**
525
+ * Get all image blocks from result content and details.
526
+ * Some tools (like generate_image) store images in details to avoid bloating
527
+ * model context. Xdev-dispatched tools preserve those details under
528
+ * details.xdev.inner.
529
+ */
530
+ #getAllImageBlocks(): ToolImageBlock[] {
531
+ if (!this.#result) return [];
532
+ const contentImages = this.#result.content.filter(block => block.type === "image");
533
+ const details = this.#result.details;
534
+ const detailImages = imageBlocksFromDetails(details);
535
+ const xdevImages = isRecord(details) && isRecord(details.xdev) ? imageBlocksFromDetails(details.xdev.inner) : [];
536
+ return [...contentImages, ...detailImages, ...xdevImages];
537
+ }
538
+
539
+ /**
540
+ * Convert non-PNG images to PNG for Kitty graphics protocol.
541
+ * Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display.
542
+ */
543
+ #maybeConvertImagesForKitty(): void {
544
+ // Only needed for Kitty protocol
545
+ if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
546
+ if (!this.#result) return;
547
+
548
+ const imageBlocks = this.#getAllImageBlocks();
549
+
550
+ for (let i = 0; i < imageBlocks.length; i++) {
551
+ const img = imageBlocks[i];
552
+ if (!img.data || !img.mimeType) continue;
553
+ // Skip if already PNG or already converted
554
+ if (img.mimeType === "image/png") continue;
555
+ if (this.#convertedImages.has(i)) continue;
556
+
557
+ // Convert async - catch errors from processing
558
+ const index = i;
559
+ convertImageToPng({ type: "image", data: img.data, mimeType: img.mimeType })
560
+ .then(converted => {
561
+ this.#convertedImages.set(index, converted);
562
+ this.#displayInputVersion++;
563
+ this.#updateDisplay();
564
+ this.#ui.requestRender();
565
+ })
566
+ .catch(() => {
567
+ // Ignore conversion failures - display will use original image format
568
+ });
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Start or stop spinner animation for live states that visibly tick.
574
+ */
575
+ #updateSpinnerAnimation(): void {
576
+ // Live partial tool blocks stay repaintable until a terminal result seals
577
+ // them. Todo snapshots and detached background tool progress are deliberate
578
+ // static exceptions because their rows can be superseded or committed to
579
+ // scrollback while later updates continue elsewhere.
580
+ const isStreamingArgs = !this.#argsComplete && (isEditLikeToolName(this.#toolName) || this.#toolName === "write");
581
+ const isBackgroundAsyncRunning =
582
+ (this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
583
+ const renderer = this.#renderer;
584
+ const pendingAnimation = renderer?.animatedPendingPreview;
585
+ const partialAnimation = renderer?.animatedPartialResult;
586
+ const pendingCallConsumesSpinner =
587
+ this.#result === undefined &&
588
+ (renderer === undefined
589
+ ? // Only the generic #formatToolExecution fallback consumes the frame;
590
+ // a custom renderCall/renderResult pair routes through the custom
591
+ // branch whose pending label is a static tool-name Text.
592
+ !this.#tool?.renderCall && !this.#tool?.renderResult
593
+ : typeof pendingAnimation === "function"
594
+ ? pendingAnimation(this.#args)
595
+ : pendingAnimation === true);
596
+ const partialResultConsumesSpinner =
597
+ this.#result !== undefined &&
598
+ (renderer === undefined
599
+ ? !this.#tool?.renderCall && !this.#tool?.renderResult
600
+ : typeof partialAnimation === "function"
601
+ ? partialAnimation(this.#args)
602
+ : partialAnimation === true);
603
+ const isLivePartialTool =
604
+ this.#isPartial &&
605
+ this.#toolName !== "todo" &&
606
+ !isBackgroundAsyncRunning &&
607
+ (pendingCallConsumesSpinner || partialResultConsumesSpinner);
608
+ const needsSpinner = isStreamingArgs || isLivePartialTool || this.#displaceableByToolName === "hub";
609
+ if (needsSpinner && !this.#spinnerActive) {
610
+ const frameCount = theme.spinnerFrames.length;
611
+ const frame = sharedSpinnerFrame(frameCount);
612
+ this.#spinnerFrame = frame;
613
+ this.#renderState.spinnerFrame = frame;
614
+ this.#spinnerActive = true;
615
+ registerSpinnerBlock(this);
616
+ } else if (!needsSpinner && this.#spinnerActive) {
617
+ this.#spinnerActive = false;
618
+ unregisterSpinnerBlock(this);
619
+ // Clear the last drawn frame so a non-live renderCall (e.g. a write whose
620
+ // args just completed) stops showing a frozen spinner glyph. Skip when a
621
+ // todo strike owns the frame — it sets its own value right after this.
622
+ if (!this.#todoStrikeInterval) {
623
+ this.#spinnerFrame = undefined;
624
+ this.#renderState.spinnerFrame = undefined;
625
+ }
626
+ }
627
+ }
628
+
629
+ /**
630
+ * Advance to the shared spinner glyph and repaint just this block. Driven by
631
+ * the single shared spinner ticker (see `registerSpinnerBlock`); the tick is
632
+ * component-scoped so the TUI reuses every other root subtree (issue #4377).
633
+ */
634
+ tickSpinner(frame: number): void {
635
+ this.#spinnerFrame = frame;
636
+ this.#renderState.spinnerFrame = frame;
637
+ this.#ui.requestComponentRender(this);
638
+ }
639
+
640
+ #updateTodoStrikeAnimation(): void {
641
+ if (this.#toolName !== "todo" || this.#isPartial || this.#result?.isError) {
642
+ this.#stopTodoStrikeAnimation();
643
+ return;
644
+ }
645
+ const completedTasks = (this.#result?.details as { completedTasks?: unknown[] } | undefined)?.completedTasks;
646
+ if (!completedTasks || completedTasks.length === 0) {
647
+ this.#stopTodoStrikeAnimation();
648
+ return;
649
+ }
650
+ if (this.#todoStrikeInterval) return;
651
+
652
+ this.#spinnerFrame = 0;
653
+ this.#renderState.spinnerFrame = 0;
654
+ this.#todoStrikeInterval = setInterval(() => {
655
+ const nextFrame = (this.#spinnerFrame ?? 0) + 1;
656
+ if (nextFrame > TODO_STRIKE_TOTAL_FRAMES) {
657
+ this.#stopTodoStrikeAnimation();
658
+ } else {
659
+ this.#spinnerFrame = nextFrame;
660
+ this.#renderState.spinnerFrame = nextFrame;
661
+ }
662
+ // Component-scoped: strike animation only mutates this tool block's
663
+ // glyph, so the TUI reuses every other root subtree (issue #4377).
664
+ this.#ui.requestComponentRender(this);
665
+ }, 65);
666
+ }
667
+
668
+ #stopTodoStrikeAnimation(): void {
669
+ if (this.#todoStrikeInterval) {
670
+ clearInterval(this.#todoStrikeInterval);
671
+ this.#todoStrikeInterval = undefined;
672
+ }
673
+ if (!this.#spinnerActive) {
674
+ this.#spinnerFrame = undefined;
675
+ this.#renderState.spinnerFrame = undefined;
676
+ }
677
+ }
678
+
679
+ /**
680
+ * Whether this block is ready to retire as immutable history. Partial
681
+ * results, including detached background tasks, remain active and mutable
682
+ * until they settle. Hidden blocks render no rows and cannot gate history.
683
+ */
684
+ isTranscriptBlockFinalized(): boolean {
685
+ if (!this.#toolActivityVisible) return true;
686
+ if (this.#sealed) return true;
687
+ if (this.#result === undefined) return false;
688
+ // A parked background task's call already returned; job frames that land
689
+ // while it is still live keep updating it, but it must not gate history.
690
+ if (this.#parkedBackground) return true;
691
+ return !this.#isPartial;
692
+ }
693
+
694
+ /**
695
+ * Subagent ids visible on this card for click-to-focus hit-testing. Empty
696
+ * unless this is a task card whose details already name spawned agents.
697
+ * Callers intersect with the live registry, which decides focusability.
698
+ */
699
+ getClickFocusAgentIds(): string[] {
700
+ if (this.#toolName !== "task") return [];
701
+ return taskCardAgentIds(this.#result?.details);
702
+ }
703
+
704
+ getTranscriptBlockVersion(): number {
705
+ return this.#blockVersion;
706
+ }
707
+ /** Mark the call parked: it returned, but stays tracked for async job frames. */
708
+ parkAsBackground(): void {
709
+ this.#parkedBackground = true;
710
+ }
711
+
712
+ /**
713
+ * Mark the tool terminal even though no result arrived (the turn aborted or
714
+ * abandoned it) and stop animating so the container can retire it.
715
+ */
716
+ seal(): void {
717
+ if (this.#sealed) return;
718
+ this.#sealed = true;
719
+ this.#blockVersion++;
720
+ this.#displaceableByToolName = undefined;
721
+ this.stopAnimation();
722
+ this.#updateDisplay();
723
+ this.#ui.requestRender();
724
+ }
725
+
726
+ /**
727
+ * Whether this block is a supersedable result snapshot that has not been
728
+ * sealed. Displacement is best-effort: the snapshot finalizes like any other
729
+ * block, so under capacity pressure it may retire to native scrollback
730
+ * first — then the follow-up call appends a fresh snapshot instead of
731
+ * removing this one (see {@link TranscriptContainer.canRemoveBlock}).
732
+ */
733
+ isDisplaceableBlock(): boolean {
734
+ return this.#displaceableByToolName !== undefined && !this.#sealed;
735
+ }
736
+
737
+ canBeDisplacedBy(nextToolName: string | undefined): boolean {
738
+ return (
739
+ this.#displaceableByToolName !== undefined && this.#displaceableByToolName === nextToolName && !this.#sealed
740
+ );
741
+ }
742
+
743
+ /**
744
+ * A live block torn down through the generic Container path (transcript
745
+ * clear, session switch mid-run) must not leak its shared-ticker
746
+ * registration: the dead component would keep the process-wide 80ms
747
+ * interval alive and keep being repainted.
748
+ */
749
+ override dispose(): void {
750
+ this.stopAnimation();
751
+ super.dispose();
752
+ }
753
+
754
+ /**
755
+ * Stop spinner animation and cleanup resources.
756
+ */
757
+ stopAnimation(): void {
758
+ if (this.#spinnerActive) {
759
+ this.#spinnerActive = false;
760
+ unregisterSpinnerBlock(this);
761
+ this.#spinnerFrame = undefined;
762
+ this.#renderState.spinnerFrame = undefined;
763
+ }
764
+ this.#stopTodoStrikeAnimation();
765
+ this.#previewReady?.resolve();
766
+ }
767
+
768
+ setExpanded(expanded: boolean): void {
769
+ if (this.#expanded !== expanded) this.#blockVersion++;
770
+ this.#expanded = expanded;
771
+ this.#updateDisplay();
772
+ }
773
+
774
+ /** Apply the transcript allocator's current viewport reservation. */
775
+ setTranscriptAllocation(rows: number, frame: AnimationFrame): void {
776
+ this.#allocation = Math.max(0, Math.trunc(rows));
777
+ this.#presentationFrame = frame;
778
+ }
779
+
780
+ setToolActivityVisible(visible: boolean): void {
781
+ this.#toolActivityVisible = visible;
782
+ super.invalidate();
783
+ }
784
+
785
+ setShowImages(show: boolean): void {
786
+ this.#showImages = show;
787
+ this.#updateDisplay();
788
+ }
789
+
790
+ override invalidate(): void {
791
+ super.invalidate();
792
+ this.#updateDisplay();
793
+ }
794
+
795
+ #updateDisplay(): void {
796
+ // `TERMINAL.imageProtocol` is resolved by an async capability probe during
797
+ // TUI startup, so a result rendered before it lands must re-shape once it
798
+ // does (it gates Image children vs text fallback in #rebuildDisplay); keyed
799
+ // here for the same reason markdown.ts keys its render cache on it.
800
+ const key = `${this.#resultVersion}|${this.#expanded}|${this.#isPartial}|${this.#argsComplete ? "1" : "0"}|${this.#executionStarted ? "1" : "0"}|${this.#spinnerFrame ?? "-"}|${this.#showImages}|${getThemeEpoch()}|${this.#displayInputVersion}|${TERMINAL.imageProtocol ?? "-"}|${this.#imageSizeKey()}`;
801
+ if (key === this.#lastDisplayKey && this.#displayBuilt) return;
802
+ this.#lastDisplayKey = key;
803
+
804
+ this.#rebuildDisplay();
805
+ this.#displayBuilt = true;
806
+ }
807
+
808
+ #rendererFlag(name: "forceResultViewportRepaintOnSettle"): boolean {
809
+ const toolValue = (this.#tool as Record<string, unknown> | undefined)?.[name];
810
+ const rendererValue = this.#renderer?.[name];
811
+ return toolValue === true || (toolValue === undefined && rendererValue === true);
812
+ }
813
+
814
+ /**
815
+ * True while the last painted pending-call shape opted into a full viewport
816
+ * repaint at the first result (`forceFirstResultViewportRepaint`) — e.g. a
817
+ * collapsed write tail window, which the first result render re-anchors
818
+ * instead of preserving. Kept as a per-paint fact so a topology-changing update that
819
+ * lands before the pending rows reach the terminal skips the reset.
820
+ */
821
+ #needsFirstResultViewportRepaintAtRender(): boolean {
822
+ if (this.#result !== undefined) return false;
823
+ const toolValue = (this.#tool as { forceFirstResultViewportRepaint?: FirstResultViewportRepaint } | undefined)
824
+ ?.forceFirstResultViewportRepaint;
825
+ const value = toolValue !== undefined ? toolValue : this.#renderer?.forceFirstResultViewportRepaint;
826
+ if (typeof value === "function") return value(this.#args, this.#renderState);
827
+ return value === true;
828
+ }
829
+
830
+ #resetDisplayForResultTopologyChange(
831
+ firstResultAfterRepaintShapePaint: boolean,
832
+ partialResultPaintedBeforeSettle: boolean,
833
+ isPartial: boolean,
834
+ ): void {
835
+ const provisionalResultSettled =
836
+ partialResultPaintedBeforeSettle && !isPartial && this.#rendererFlag("forceResultViewportRepaintOnSettle");
837
+ if (firstResultAfterRepaintShapePaint || provisionalResultSettled) {
838
+ this.#ui.requestRender();
839
+ }
840
+ }
841
+
842
+ override render(width: number): readonly string[] {
843
+ if (!this.#toolActivityVisible || this.#allocation === 0) return [];
844
+ let lines = super.render(width);
845
+ if (this.#allocation < 3) {
846
+ // A squeezed allocation degrades only blocks that genuinely overflow it.
847
+ // The allocator measures blocks by trimmed height and never squeezes one
848
+ // below that, so inline tools whose real content is 1-2 rows (hub
849
+ // receipts, one-line results) keep that content instead of an equally
850
+ // tall but contentless frame.
851
+ const trimmed = trimBlankEdges(lines);
852
+ if (trimmed.length > this.#allocation) return this.#renderCompact(width);
853
+ lines = trimmed;
854
+ }
855
+ this.#firstResultViewportRepaintShapePainted = this.#needsFirstResultViewportRepaintAtRender();
856
+ this.#partialResultShapePainted = this.#result !== undefined && this.#isPartial;
857
+ return lines;
858
+ }
859
+
860
+ #renderCompact(width: number): readonly string[] {
861
+ const summary = this.#activitySummary();
862
+ const detail = summary.detail ? theme.fg("muted", ` · ${summary.detail.replace(/\s+/g, " ")}`) : "";
863
+ // Elapsed ticks only while the call is genuinely running; a settled
864
+ // placeholder row must not read as live ("Todo · running 0s").
865
+ const elapsed =
866
+ this.#isRunning() && this.#executionStartedAtNow !== undefined
867
+ ? theme.fg(
868
+ "dim",
869
+ ` ${Math.max(0, Math.floor((this.#presentationFrame.now - this.#executionStartedAtNow) / 1000))}s`,
870
+ )
871
+ : "";
872
+ const text = truncateToWidth(
873
+ `${theme.fg("toolTitle", theme.bold(summary.label))}${detail}${elapsed}`,
874
+ Math.max(1, width - 4),
875
+ );
876
+ if (this.#allocation === 1) {
877
+ const glyph = this.#spinnerFrame === undefined ? "•" : (theme.spinnerFrames[this.#spinnerFrame] ?? "•");
878
+ const styledGlyph = theme.fg(this.#spinnerFrame === undefined ? "dim" : "muted", glyph);
879
+ return [truncateToWidth(`${styledGlyph} ${text}`, width)];
880
+ }
881
+ return [truncateToWidth(`${theme.fg("dim", "╭─")} ${text}`, width), theme.fg("dim", "╰")];
882
+ }
883
+
884
+ #activitySummary(): ToolActivitySummary {
885
+ this.#renderState.renderContext ??= this.#buildRenderContext();
886
+ const summary = this.#renderer?.activitySummary?.(this.#args, {
887
+ expanded: this.#expanded,
888
+ isPartial: this.#isPartial,
889
+ spinnerFrame: this.#spinnerFrame,
890
+ renderContext: this.#renderState.renderContext,
891
+ });
892
+ if (summary !== undefined) {
893
+ if (summary.detail) return summary;
894
+ // A detail-less custom summary (e.g. hub before its streamed args
895
+ // parse) must not fold to a bare `╭─ Label` frame under viewport
896
+ // pressure — keep the generic liveness hint for in-flight calls.
897
+ return this.#isRunning() ? { ...summary, detail: "running" } : summary;
898
+ }
899
+ if (isRecord(this.#args)) {
900
+ for (const key of ["command", "path", "input"] as const) {
901
+ const value = this.#args[key];
902
+ if (typeof value === "string" && value.length > 0) {
903
+ return { label: this.#toolLabel, detail: value.split("\n", 1)[0] };
904
+ }
905
+ }
906
+ }
907
+ return { label: this.#toolLabel, detail: this.#isRunning() ? "running" : undefined };
908
+ }
909
+ /** Still executing: no settled result yet and the turn has not sealed it. */
910
+ #isRunning(): boolean {
911
+ return !this.#sealed && (this.#result === undefined || this.#isPartial);
912
+ }
913
+
914
+ // Viewport-/settings-dependent image sizing folded into the memo key only when
915
+ // the last rebuild actually emitted images, so a terminal resize re-shapes an
916
+ // image-bearing result (to rescale it) without re-shaping every image-free
917
+ // result on each resize tick.
918
+ #imageSizeKey(): string {
919
+ if (this.#renderedImageCount === 0) return "-";
920
+ const o = resolveImageOptions();
921
+ return `${o.maxWidthCells}:${o.maxHeightCells ?? "-"}`;
922
+ }
923
+
924
+ #rebuildDisplay(): void {
925
+ // Sync shared mutable render state for component closures
926
+ this.#renderState.expanded = this.#expanded;
927
+ this.#renderState.isPartial = this.#isPartial;
928
+ this.#renderState.argsComplete = this.#argsComplete;
929
+ this.#renderState.executionStarted = this.#executionStarted;
930
+ this.#renderState.spinnerFrame = this.#spinnerFrame;
931
+
932
+ // Non-self-framing tools (custom/extension renderers and the generic
933
+ // fallback) get a padded, state-tinted block — built-ins that draw their
934
+ // own frame opt out below via the framed-component mark. A benign skip
935
+ // (steering/peer interrupt aborted a still-pending call) never ran, so it
936
+ // gets the neutral pending tint rather than the error tint (#7199).
937
+ const benignSkip = this.#isBenignSkip();
938
+ const stateBgKey =
939
+ this.#isPartial || benignSkip ? "toolPendingBg" : this.#result?.isError ? "toolErrorBg" : "toolSuccessBg";
940
+ const stateBgFn = (t: string) => theme.bg(stateBgKey, t);
941
+
942
+ // A benign skip is a synthetic placeholder for a call that never executed,
943
+ // so bypass any bespoke error frame and draw the neutral generic card —
944
+ // the per-tool ✘/red-border would misread normal mid-turn steering as a
945
+ // failure (#7199).
946
+ if (benignSkip) {
947
+ this.#renderBenignSkipCard(stateBgFn);
948
+ } else if (this.#tool && (this.#tool.renderCall || this.#tool.renderResult)) {
949
+ const tool = this.#tool;
950
+ const mergeCallAndResult = Boolean((tool as { mergeCallAndResult?: boolean }).mergeCallAndResult);
951
+ // Custom tools use Box for flexible component rendering
952
+ this.#contentBox.setBgFn(undefined);
953
+ this.#contentBox.clear();
954
+ // Mirror the built-in renderer branch so custom renderers (notably the
955
+ // task tool, whose live instance routes through here) receive the same
956
+ // render context — e.g. the `hasResult` flag that suppresses the task
957
+ // call preview once result lines exist.
958
+ this.#renderState.renderContext = this.#buildRenderContext();
959
+
960
+ // Render call component. The fallback label only stands in for a
961
+ // missing `renderCall`; when the call is intentionally suppressed
962
+ // (mergeCallAndResult once a result exists) we render nothing here so
963
+ // the result component isn't preceded by a redundant tool-name line.
964
+ const shouldRenderCall = !this.#result || !mergeCallAndResult;
965
+ if (shouldRenderCall) {
966
+ if (tool.renderCall) {
967
+ try {
968
+ const callArgs = this.#getCallArgsForRender();
969
+ const callComponent = tool.renderCall(callArgs, this.#renderState, theme) as Component | undefined;
970
+ if (callComponent) {
971
+ this.#contentBox.addChild(
972
+ new SafeToolRendererComponent(
973
+ this.#toolName,
974
+ "call",
975
+ callComponent,
976
+ () => new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0),
977
+ ),
978
+ );
979
+ }
980
+ } catch (err) {
981
+ logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
982
+ // Fall back to default on error
983
+ this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
984
+ }
985
+ } else {
986
+ // No custom renderCall, show tool name
987
+ this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
988
+ }
989
+ }
990
+
991
+ // Render result component if we have a result
992
+ if (this.#result && tool.renderResult) {
993
+ try {
994
+ const renderResult = tool.renderResult as (
995
+ result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
996
+ options: { expanded: boolean; isPartial: boolean; spinnerFrame?: number },
997
+ theme: Theme,
998
+ args?: unknown,
999
+ ) => Component;
1000
+ const resultComponent = renderResult(
1001
+ {
1002
+ content: this.#result.content,
1003
+ details: this.#result.details,
1004
+ isError: this.#result.isError,
1005
+ },
1006
+ this.#renderState,
1007
+ theme,
1008
+ this.#args,
1009
+ );
1010
+ if (resultComponent) {
1011
+ this.#contentBox.addChild(
1012
+ new SafeToolRendererComponent(this.#toolName, "result", resultComponent, () => {
1013
+ const output = this.#getTextOutput();
1014
+ if (!output) return undefined;
1015
+ return new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0);
1016
+ }),
1017
+ );
1018
+ }
1019
+ } catch (err) {
1020
+ logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
1021
+ // Fall back to showing raw output on error
1022
+ const output = this.#getTextOutput();
1023
+ if (output) {
1024
+ this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
1025
+ }
1026
+ }
1027
+ } else if (this.#result) {
1028
+ // Has result but no custom renderResult
1029
+ const output = this.#getTextOutput();
1030
+ if (output) {
1031
+ this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
1032
+ }
1033
+ }
1034
+ // Custom tools that draw their own frame (task) render flush; plain
1035
+ // extension renderers get the padded, state-tinted block back.
1036
+ const customFramed = this.#contentBox.children.some(isFramedBlockComponent);
1037
+ this.#contentBox.setPaddingX(customFramed ? 0 : 1);
1038
+ this.#contentBox.setBgFn(customFramed ? undefined : stateBgFn);
1039
+ } else if (this.#renderer) {
1040
+ // The active registry entry is a built-in tool with a rich renderer.
1041
+ const renderer = this.#renderer;
1042
+
1043
+ // Clean up previous multi-file boxes
1044
+ for (const box of this.#multiFileBoxes) {
1045
+ this.removeChild(box);
1046
+ }
1047
+ this.#multiFileBoxes = [];
1048
+
1049
+ // Check for multi-file edit results
1050
+ const details = this.#result?.details;
1051
+ const perFileResults = (isRecord(details) ? details.perFileResults : undefined) as
1052
+ | Array<{ path: string; isError?: boolean }>
1053
+ | undefined;
1054
+ if (perFileResults && perFileResults.length > 1) {
1055
+ // Multi-file: render each file as its own Box (identical to separate tool calls)
1056
+ this.#contentBox.setBgFn(undefined);
1057
+ this.#contentBox.clear();
1058
+
1059
+ const renderContext = this.#buildRenderContext();
1060
+ this.#renderState.renderContext = renderContext;
1061
+
1062
+ for (let i = 0; i < perFileResults.length; i++) {
1063
+ const fileResult = perFileResults[i];
1064
+ if (i > 0) {
1065
+ const spacer = new Spacer(1);
1066
+ this.#multiFileBoxes.push(spacer);
1067
+ this.addChild(spacer);
1068
+ }
1069
+ const fileBox = new Box(0, 0);
1070
+ try {
1071
+ const resultComponent = renderer.renderResult(
1072
+ { content: [], details: fileResult, isError: fileResult.isError },
1073
+ this.#renderState,
1074
+ theme,
1075
+ );
1076
+ if (resultComponent) {
1077
+ fileBox.addChild(
1078
+ new SafeToolRendererComponent(this.#toolName, "result", resultComponent, () => undefined),
1079
+ );
1080
+ }
1081
+ } catch (err) {
1082
+ logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
1083
+ }
1084
+ this.#multiFileBoxes.push(fileBox);
1085
+ this.addChild(fileBox);
1086
+ }
1087
+
1088
+ // Show pending indicator for remaining files
1089
+ const edits = isRecord(this.#args) ? this.#args.edits : undefined;
1090
+ const totalFiles = Array.isArray(edits)
1091
+ ? new Set(edits.map((entry: unknown) => (isRecord(entry) ? entry.path : undefined)).filter(Boolean)).size
1092
+ : 0;
1093
+ const remaining = Math.max(0, totalFiles - perFileResults.length);
1094
+ if (remaining > 0 && this.#isPartial) {
1095
+ const pendingSpacer = new Spacer(1);
1096
+ this.#multiFileBoxes.push(pendingSpacer);
1097
+ this.addChild(pendingSpacer);
1098
+ const pendingBox = new Box(0, 0);
1099
+ const spinner =
1100
+ this.#spinnerFrame !== undefined ? formatStatusIcon("running", theme, this.#spinnerFrame) : "";
1101
+ const pendingText = renderStatusLine(
1102
+ {
1103
+ iconOverride: spinner,
1104
+ title: "Edit",
1105
+ description: theme.fg("dim", `${remaining} more file${remaining > 1 ? "s" : ""} pending…`),
1106
+ },
1107
+ theme,
1108
+ );
1109
+ pendingBox.addChild(new Text(pendingText, 0, 0));
1110
+ this.#multiFileBoxes.push(pendingBox);
1111
+ this.addChild(pendingBox);
1112
+ }
1113
+ } else {
1114
+ // Single-file or no result: standard rendering
1115
+ // Inline renderers skip background styling
1116
+ this.#contentBox.setBgFn(undefined);
1117
+ this.#contentBox.clear();
1118
+
1119
+ const renderContext = this.#buildRenderContext();
1120
+ this.#renderState.renderContext = renderContext;
1121
+
1122
+ const shouldRenderCall = !this.#result || !renderer.mergeCallAndResult;
1123
+ if (shouldRenderCall) {
1124
+ // Render call component
1125
+ try {
1126
+ const callArgs = this.#getCallArgsForRender();
1127
+ const callComponent = renderer.renderCall(callArgs, this.#renderState, theme);
1128
+ if (callComponent) {
1129
+ this.#contentBox.addChild(
1130
+ new SafeToolRendererComponent(
1131
+ this.#toolName,
1132
+ "call",
1133
+ callComponent,
1134
+ () => new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0),
1135
+ ),
1136
+ );
1137
+ }
1138
+ } catch (err) {
1139
+ logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
1140
+ // Fall back to default on error
1141
+ this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
1142
+ }
1143
+ }
1144
+
1145
+ // Render result component if we have a result
1146
+ if (this.#result) {
1147
+ try {
1148
+ const resultComponent = renderer.renderResult(
1149
+ {
1150
+ content: this.#result.content,
1151
+ details: this.#result.details,
1152
+ isError: this.#result.isError,
1153
+ },
1154
+ this.#renderState,
1155
+ theme,
1156
+ this.#getCallArgsForRender(),
1157
+ );
1158
+ if (resultComponent) {
1159
+ this.#contentBox.addChild(
1160
+ new SafeToolRendererComponent(this.#toolName, "result", resultComponent, () => {
1161
+ const output = this.#getTextOutput();
1162
+ if (!output) return undefined;
1163
+ return new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0);
1164
+ }),
1165
+ );
1166
+ }
1167
+ } catch (err) {
1168
+ logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
1169
+ // Fall back to showing raw output on error
1170
+ const output = this.#getTextOutput();
1171
+ if (output) {
1172
+ this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
1173
+ }
1174
+ }
1175
+ }
1176
+ }
1177
+ } else {
1178
+ // Generic fallback (no custom/built-in renderer). WidthAwareText
1179
+ // reformats at render time so output fills the actual terminal width
1180
+ // instead of a fixed column cap.
1181
+ this.#contentText.setCustomBgFn(stateBgFn);
1182
+ this.#contentText.invalidate();
1183
+ }
1184
+
1185
+ // Handle images (same for both custom and built-in)
1186
+ for (const img of this.#imageComponents) {
1187
+ this.removeChild(img);
1188
+ }
1189
+ this.#imageComponents = [];
1190
+ for (const spacer of this.#imageSpacers) {
1191
+ this.removeChild(spacer);
1192
+ }
1193
+ this.#imageSpacers = [];
1194
+
1195
+ if (this.#result) {
1196
+ const imageBlocks = this.#getAllImageBlocks();
1197
+
1198
+ for (let i = 0; i < imageBlocks.length; i++) {
1199
+ const img = imageBlocks[i];
1200
+ if (TERMINAL.imageProtocol && this.#showImages && img.data && img.mimeType) {
1201
+ // Use converted PNG for Kitty protocol if available
1202
+ const converted = this.#convertedImages.get(i);
1203
+ const imageData = converted?.data ?? img.data;
1204
+ const imageMimeType = converted?.mimeType ?? img.mimeType;
1205
+
1206
+ // For Kitty, skip non-PNG images that haven't been converted yet
1207
+ if (TERMINAL.imageProtocol === ImageProtocol.Kitty && imageMimeType !== "image/png") {
1208
+ continue;
1209
+ }
1210
+
1211
+ const spacer = new Spacer(1);
1212
+ this.addChild(spacer);
1213
+ this.#imageSpacers.push(spacer);
1214
+ const imageComponent = new Image(
1215
+ imageData,
1216
+ imageMimeType,
1217
+ { fallbackColor: (s: string) => theme.fg("toolOutput", s) },
1218
+ { ...resolveImageOptions(), budget: this.#ui.imageBudget, imageKey: `te${this.#instanceId}:${i}` },
1219
+ );
1220
+ this.#imageComponents.push(imageComponent);
1221
+ this.addChild(imageComponent);
1222
+ }
1223
+ }
1224
+ }
1225
+ this.#renderedImageCount = this.#imageComponents.length;
1226
+ }
1227
+
1228
+ #getCallArgsForRender(): unknown {
1229
+ const renderArgs = this.#args;
1230
+ if (!isEditLikeToolName(this.#toolName)) {
1231
+ return renderArgs;
1232
+ }
1233
+ const previews = this.#editDiffPreview;
1234
+ if (!previews || previews.length === 0) {
1235
+ return renderArgs;
1236
+ }
1237
+ // Single-file previews feed the existing `previewDiff` channel consumed
1238
+ // by `formatStreamingDiff` in the renderer.
1239
+ const first = previews[0];
1240
+ if (!first?.diff) {
1241
+ return renderArgs;
1242
+ }
1243
+ return { ...(renderArgs as Record<string, unknown>), previewDiff: first.diff };
1244
+ }
1245
+
1246
+ /**
1247
+ * Build render context for tools that need extra state (bash, python, edit)
1248
+ */
1249
+ #buildRenderContext(): Record<string, unknown> {
1250
+ const context: Record<string, unknown> = {};
1251
+ const normalizeTimeoutSeconds = (value: unknown, maxSeconds: number): number | undefined => {
1252
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
1253
+ return Math.max(1, Math.min(maxSeconds, value));
1254
+ };
1255
+
1256
+ if (this.#toolName === "bash") {
1257
+ // Bash needs render context even before a result exists. The renderer uses the pending-call args
1258
+ // plus this context to keep the inline command preview visible while tool-call JSON is still streaming.
1259
+ if (this.#result) {
1260
+ // Pass raw output and expanded state - renderer handles width-aware truncation
1261
+ const output = this.#getTextOutput().trimEnd();
1262
+ context.output = output;
1263
+ }
1264
+ context.expanded = this.#expanded;
1265
+ context.previewLines = BASH_DEFAULT_PREVIEW_LINES;
1266
+ context.timeout = normalizeTimeoutSeconds(isRecord(this.#args) ? this.#args.timeout : undefined, 3600);
1267
+ } else if (this.#toolName === "eval" && this.#result) {
1268
+ const output = this.#getTextOutput().trimEnd();
1269
+ context.output = output;
1270
+ context.expanded = this.#expanded;
1271
+ context.previewLines = EVAL_DEFAULT_PREVIEW_LINES;
1272
+ } else if (this.#toolName === "task") {
1273
+ // Once a result snapshot exists the task renderer's `renderResult`
1274
+ // draws every dispatched agent as a progress/result line, so tell
1275
+ // `renderCall` to drop its duplicate streaming preview list.
1276
+ context.hasResult = Boolean(this.#result);
1277
+ this.#taskRenderNowMs = Date.now();
1278
+ context.nowMs = this.#taskRenderNowMs;
1279
+ } else if (isEditLikeToolName(this.#toolName)) {
1280
+ context.editMode = this.#editMode;
1281
+ const previews = this.#editDiffPreview;
1282
+ if (previews && previews.length > 0) {
1283
+ const first = previews[0];
1284
+ if (first?.diff || first?.error) {
1285
+ context.editDiffPreview = first.error
1286
+ ? { error: first.error }
1287
+ : { diff: first.diff ?? "", firstChangedLine: first.firstChangedLine };
1288
+ }
1289
+ if (previews.length > 1) {
1290
+ context.perFileDiffPreview = previews;
1291
+ }
1292
+ }
1293
+ if (!previews?.some(preview => preview.diff)) {
1294
+ const editMode = this.#editMode;
1295
+ const fallback = editMode ? renderStreamingFallback(editMode, this.#args, theme) : "";
1296
+ if (fallback) context.editStreamingFallback = fallback;
1297
+ }
1298
+ context.renderDiff = renderDiff;
1299
+ } else if (this.#toolName === "write") {
1300
+ // Device-dispatch previews resolve renderers from the canonical tool map.
1301
+ const writeTool = this.#tool as { session?: { xdev?: XdevMountedState } } | undefined;
1302
+ const xdev = writeTool?.session?.xdev;
1303
+ if (xdev) {
1304
+ context.resolveXdevMounted = (name: string) =>
1305
+ xdev.mountedNames.has(name) ? xdev.tools.get(name) : undefined;
1306
+ }
1307
+ }
1308
+
1309
+ return context;
1310
+ }
1311
+
1312
+ #getTextOutput(): string {
1313
+ if (!this.#result) return "";
1314
+
1315
+ const textBlocks = this.#result.content.filter(c => c.type === "text");
1316
+ const imageBlocks = this.#getAllImageBlocks();
1317
+
1318
+ let output = textBlocks
1319
+ .map(c => {
1320
+ return sanitizeWithOptionalSixelPassthrough(c.text || "", sanitizeText);
1321
+ })
1322
+ .join("\n");
1323
+
1324
+ if (imageBlocks.length > 0 && (!TERMINAL.imageProtocol || !this.#showImages)) {
1325
+ const imageIndicators = imageBlocks
1326
+ .map(img => {
1327
+ const mimeType = String(img.mimeType);
1328
+ const dims = img.data ? (getImageDimensions(img.data, mimeType) ?? undefined) : undefined;
1329
+ return imageFallback(mimeType, dims);
1330
+ })
1331
+ .join("\n");
1332
+ output = output ? `${output}\n${imageIndicators}` : imageIndicators;
1333
+ }
1334
+
1335
+ return output;
1336
+ }
1337
+
1338
+ /**
1339
+ * Format the generic call/result card at `contentWidth`. Shared by the
1340
+ * #contentText fallback and the benign-skip path so both render identically.
1341
+ */
1342
+ #renderDefaultCard(contentWidth: number): string {
1343
+ return formatDefaultToolExecution(
1344
+ {
1345
+ label: this.#toolLabel,
1346
+ args: this.#args,
1347
+ result: this.#result
1348
+ ? { output: this.#getTextOutput(), isError: this.#result.isError, skipped: this.#isBenignSkip() }
1349
+ : undefined,
1350
+ options: this.#renderState,
1351
+ },
1352
+ contentWidth,
1353
+ theme,
1354
+ );
1355
+ }
1356
+
1357
+ /**
1358
+ * True for a steering/peer-interrupt placeholder. A synthetic placeholder
1359
+ * identifies a call that never entered `tool.execute`; an interrupted
1360
+ * placeholder identifies one that started but threw before returning usable
1361
+ * output. Both are normal steering control flow and render neutrally (#7199).
1362
+ */
1363
+ #isBenignSkip(): boolean {
1364
+ if (this.#isPartial || !this.#result) return false;
1365
+ const details = this.#result.details as
1366
+ | { __synthetic?: boolean; __interrupted?: boolean; source?: string; execution?: string }
1367
+ | undefined;
1368
+ if (details?.source !== "interrupt_skipped") return false;
1369
+ return details.__synthetic === true || (details.__interrupted === true && details.execution === "started");
1370
+ }
1371
+
1372
+ /**
1373
+ * Render a benign skip as the neutral generic card, replacing any bespoke
1374
+ * renderer's error frame. Generic-fallback tools already route through
1375
+ * {@link #renderDefaultCard} (which emits the info card for a skip); they
1376
+ * only need the neutral tint. Bespoke-renderer tools get their content box
1377
+ * swapped for the same neutral card.
1378
+ */
1379
+ #renderBenignSkipCard(stateBgFn: (text: string) => string): void {
1380
+ if (!this.#usesContentBox) {
1381
+ this.#contentText.setCustomBgFn(stateBgFn);
1382
+ this.#contentText.invalidate();
1383
+ return;
1384
+ }
1385
+ for (const box of this.#multiFileBoxes) {
1386
+ this.removeChild(box);
1387
+ }
1388
+ this.#multiFileBoxes = [];
1389
+ this.#contentBox.setBgFn(undefined);
1390
+ this.#contentBox.clear();
1391
+ this.#contentBox.setPaddingX(1);
1392
+ this.#contentBox.setBgFn(stateBgFn);
1393
+ this.#contentBox.addChild(new WidthAwareText(contentWidth => this.#renderDefaultCard(contentWidth), 0, 0));
1394
+ }
1395
+ }