@oh-my-pi/pi-tui 18.2.4 → 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 (748) hide show
  1. package/CHANGELOG.md +23 -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/key-value-list.d.ts +24 -0
  92. package/dist/types/components/layout/geometry.d.ts +73 -0
  93. package/dist/types/components/layout/row.d.ts +55 -0
  94. package/dist/types/components/layout/split-pane.d.ts +80 -0
  95. package/dist/types/components/layout/stack.d.ts +50 -0
  96. package/dist/types/components/menu-selection.d.ts +71 -0
  97. package/dist/types/components/metric.d.ts +44 -0
  98. package/dist/types/components/progress-bar.d.ts +36 -0
  99. package/dist/types/components/scroll-view.d.ts +53 -19
  100. package/dist/types/components/scroll-viewport.d.ts +46 -0
  101. package/dist/types/components/section.d.ts +19 -0
  102. package/dist/types/components/select-list.d.ts +46 -2
  103. package/dist/types/components/settings-list.d.ts +20 -0
  104. package/dist/types/components/table.d.ts +32 -0
  105. package/dist/types/components/tree-view.d.ts +130 -0
  106. package/dist/types/components/wizard-step.d.ts +60 -0
  107. package/dist/types/hotkeys-markdown.d.ts +7 -0
  108. package/dist/types/index.d.ts +17 -0
  109. package/dist/types/keybinding-matchers.d.ts +35 -0
  110. package/dist/types/keys.d.ts +2 -2
  111. package/dist/types/lang-from-path.d.ts +9 -0
  112. package/dist/types/overlays/advisor-config.d.ts +114 -0
  113. package/dist/types/overlays/agent-activity.d.ts +41 -0
  114. package/dist/types/overlays/agent-hub-projection.d.ts +33 -0
  115. package/dist/types/overlays/agent-hub-renderer.d.ts +53 -0
  116. package/dist/types/overlays/agent-hub-types.d.ts +99 -0
  117. package/dist/types/overlays/agent-hub.d.ts +126 -0
  118. package/dist/types/overlays/agent-transcript-viewer.d.ts +71 -0
  119. package/dist/types/overlays/agents-hub.d.ts +72 -0
  120. package/dist/types/overlays/ask-dialog.d.ts +88 -0
  121. package/dist/types/overlays/bordered-loader.d.ts +11 -0
  122. package/dist/types/overlays/btw-history-panel.d.ts +29 -0
  123. package/dist/types/overlays/btw-history.d.ts +17 -0
  124. package/dist/types/overlays/btw-panel.d.ts +26 -0
  125. package/dist/types/overlays/cleanse-panel.d.ts +36 -0
  126. package/dist/types/overlays/codex-reset-fireworks.d.ts +52 -0
  127. package/dist/types/overlays/composer-shape-preview.d.ts +47 -0
  128. package/dist/types/overlays/composer-shape-registry.d.ts +50 -0
  129. package/dist/types/overlays/copy-selector.d.ts +48 -0
  130. package/dist/types/overlays/copy-targets.d.ts +63 -0
  131. package/dist/types/overlays/error-banner.d.ts +13 -0
  132. package/dist/types/overlays/extensions/display-text.d.ts +10 -0
  133. package/dist/types/overlays/extensions/extension-dashboard.d.ts +63 -0
  134. package/dist/types/overlays/extensions/extension-list.d.ts +54 -0
  135. package/dist/types/overlays/extensions/inspector-model.d.ts +109 -0
  136. package/dist/types/overlays/extensions/inspector-panel.d.ts +17 -0
  137. package/dist/types/overlays/extensions/live-tool-session.d.ts +28 -0
  138. package/dist/types/overlays/extensions/mcp-runtime.d.ts +107 -0
  139. package/dist/types/overlays/extensions/state-manager.d.ts +42 -0
  140. package/dist/types/overlays/extensions/types.d.ts +165 -0
  141. package/dist/types/overlays/history-search.d.ts +16 -0
  142. package/dist/types/overlays/hook-editor.d.ts +44 -0
  143. package/dist/types/overlays/hook-input.d.ts +20 -0
  144. package/dist/types/overlays/hook-selector.d.ts +71 -0
  145. package/dist/types/overlays/hub-frame.d.ts +77 -0
  146. package/dist/types/overlays/login-dialog.d.ts +43 -0
  147. package/dist/types/overlays/logout-account-selector.d.ts +15 -0
  148. package/dist/types/overlays/mcp-add-wizard.d.ts +87 -0
  149. package/dist/types/overlays/model-browser.d.ts +213 -0
  150. package/dist/types/overlays/model-hub.d.ts +72 -0
  151. package/dist/types/overlays/model-picker.d.ts +74 -0
  152. package/dist/types/overlays/model-selector.d.ts +52 -0
  153. package/dist/types/overlays/move-overlay.d.ts +39 -0
  154. package/dist/types/overlays/oauth-selector.d.ts +41 -0
  155. package/dist/types/overlays/omfg-panel.d.ts +20 -0
  156. package/dist/types/overlays/pause-screen.d.ts +43 -0
  157. package/dist/types/overlays/plan-review-overlay.d.ts +89 -0
  158. package/dist/types/overlays/plan-save-overlay.d.ts +19 -0
  159. package/dist/types/overlays/plan-toc.d.ts +41 -0
  160. package/dist/types/overlays/plugin-selector.d.ts +28 -0
  161. package/dist/types/overlays/plugin-settings.d.ts +180 -0
  162. package/dist/types/overlays/queue-mode-selector.d.ts +11 -0
  163. package/dist/types/overlays/reset-usage-selector.d.ts +23 -0
  164. package/dist/types/overlays/rewind-selector.d.ts +61 -0
  165. package/dist/types/overlays/running-subagent-badge.d.ts +6 -0
  166. package/dist/types/overlays/session-account-selector.d.ts +24 -0
  167. package/dist/types/overlays/session-info-overlay.d.ts +17 -0
  168. package/dist/types/overlays/session-observer-registry.d.ts +77 -0
  169. package/dist/types/overlays/session-selector.d.ts +131 -0
  170. package/dist/types/overlays/settings-defs.d.ts +119 -0
  171. package/dist/types/overlays/settings-selector.d.ts +73 -0
  172. package/dist/types/overlays/show-images-selector.d.ts +11 -0
  173. package/dist/types/overlays/snapcompact-shape-preview.d.ts +31 -0
  174. package/dist/types/overlays/theme-selector.d.ts +12 -0
  175. package/dist/types/overlays/thinking-selector.d.ts +12 -0
  176. package/dist/types/overlays/tiny-title-download-progress.d.ts +21 -0
  177. package/dist/types/overlays/tree-selector.d.ts +92 -0
  178. package/dist/types/overlays/usage-dashboard.d.ts +94 -0
  179. package/dist/types/overlays/usage-display.d.ts +5 -0
  180. package/dist/types/overlays/usage-row.d.ts +17 -0
  181. package/dist/types/prompt/attachment-chips.d.ts +19 -0
  182. package/dist/types/prompt/composer-attachments.d.ts +71 -0
  183. package/dist/types/prompt/composer-cache.d.ts +37 -0
  184. package/dist/types/prompt/composer.d.ts +159 -0
  185. package/dist/types/prompt/custom-editor.d.ts +261 -0
  186. package/dist/types/prompt/editor-top-gap.d.ts +18 -0
  187. package/dist/types/prompt/emoji-autocomplete.d.ts +16 -0
  188. package/dist/types/prompt/github-ref-autocomplete.d.ts +35 -0
  189. package/dist/types/prompt/gradient-highlight.d.ts +32 -0
  190. package/dist/types/prompt/image-format.d.ts +4 -0
  191. package/dist/types/prompt/image-references.d.ts +26 -0
  192. package/dist/types/prompt/interactive-context-helpers.d.ts +43 -0
  193. package/dist/types/prompt/internal-url-autocomplete.d.ts +56 -0
  194. package/dist/types/prompt/macos-spelling.d.ts +44 -0
  195. package/dist/types/prompt/magic-keyword-boundary.d.ts +9 -0
  196. package/dist/types/prompt/magic-keywords.d.ts +26 -0
  197. package/dist/types/prompt/markdown-prose.d.ts +15 -0
  198. package/dist/types/prompt/model-mention-autocomplete.d.ts +25 -0
  199. package/dist/types/prompt/model-mention-syntax.d.ts +24 -0
  200. package/dist/types/prompt/orchestrate.d.ts +13 -0
  201. package/dist/types/prompt/prompt-action-autocomplete.d.ts +56 -0
  202. package/dist/types/prompt/queue-input.d.ts +8 -0
  203. package/dist/types/prompt/skill-tokens.d.ts +13 -0
  204. package/dist/types/prompt/tools-markdown.d.ts +12 -0
  205. package/dist/types/prompt/ultrathink.d.ts +13 -0
  206. package/dist/types/prompt/usage-amounts.d.ts +26 -0
  207. package/dist/types/prompt/video.d.ts +18 -0
  208. package/dist/types/prompt/welcome.d.ts +83 -0
  209. package/dist/types/prompt/workflow.d.ts +13 -0
  210. package/dist/types/render/code-cell.d.ts +51 -0
  211. package/dist/types/render/file-list.d.ts +22 -0
  212. package/dist/types/render/hyperlink.d.ts +80 -0
  213. package/dist/types/render/index.d.ts +16 -0
  214. package/dist/types/render/output-block.d.ts +54 -0
  215. package/dist/types/render/output-pane.d.ts +80 -0
  216. package/dist/types/render/render-utils.d.ts +312 -0
  217. package/dist/types/render/sixel.d.ts +21 -0
  218. package/dist/types/render/status-line.d.ts +23 -0
  219. package/dist/types/render/tool-card.d.ts +71 -0
  220. package/dist/types/render/tree-list.d.ts +29 -0
  221. package/dist/types/render/types.d.ts +17 -0
  222. package/dist/types/render/utils.d.ts +43 -0
  223. package/dist/types/render/width-aware-text.d.ts +23 -0
  224. package/dist/types/setup/lazy.d.ts +3 -0
  225. package/dist/types/setup/scenes/composer.d.ts +3 -0
  226. package/dist/types/setup/scenes/glyph.d.ts +3 -0
  227. package/dist/types/setup/scenes/model.d.ts +3 -0
  228. package/dist/types/setup/scenes/outro.d.ts +2 -0
  229. package/dist/types/setup/scenes/providers.d.ts +3 -0
  230. package/dist/types/setup/scenes/sign-in.d.ts +21 -0
  231. package/dist/types/setup/scenes/splash.d.ts +11 -0
  232. package/dist/types/setup/scenes/theme.d.ts +3 -0
  233. package/dist/types/setup/scenes/types.d.ts +110 -0
  234. package/dist/types/setup/scenes/web-search.d.ts +22 -0
  235. package/dist/types/setup/setup-version.d.ts +11 -0
  236. package/dist/types/setup/startup-splash.d.ts +9 -0
  237. package/dist/types/setup/wizard-overlay.d.ts +16 -0
  238. package/dist/types/setup/wizard.d.ts +25 -0
  239. package/dist/types/status-line/component.d.ts +182 -0
  240. package/dist/types/status-line/context-usage.d.ts +156 -0
  241. package/dist/types/status-line/footer.d.ts +30 -0
  242. package/dist/types/status-line/git-utils.d.ts +22 -0
  243. package/dist/types/status-line/host.d.ts +129 -0
  244. package/dist/types/status-line/index.d.ts +6 -0
  245. package/dist/types/status-line/loop.d.ts +18 -0
  246. package/dist/types/status-line/metrics.d.ts +21 -0
  247. package/dist/types/status-line/presets.d.ts +3 -0
  248. package/dist/types/status-line/schema.d.ts +15 -0
  249. package/dist/types/status-line/segments.d.ts +5 -0
  250. package/dist/types/status-line/separators.d.ts +3 -0
  251. package/dist/types/status-line/types.d.ts +236 -0
  252. package/dist/types/stdin-buffer.d.ts +4 -0
  253. package/dist/types/theme/color.d.ts +24 -0
  254. package/dist/types/theme/defaults/index.d.ts +9489 -0
  255. package/dist/types/theme/index.d.ts +4 -0
  256. package/dist/types/theme/loader.d.ts +23 -0
  257. package/dist/types/theme/mermaid-cache.d.ts +26 -0
  258. package/dist/types/theme/schema-validation.d.ts +3 -0
  259. package/dist/types/theme/schema.d.ts +25 -0
  260. package/dist/types/theme/session-color.d.ts +43 -0
  261. package/dist/types/theme/shimmer.d.ts +44 -0
  262. package/dist/types/theme/symbols.d.ts +34 -0
  263. package/dist/types/theme/theme-class.d.ts +299 -0
  264. package/dist/types/theme/theme.d.ts +111 -0
  265. package/dist/types/theme/tui-adapters.d.ts +30 -0
  266. package/dist/types/thinking.d.ts +165 -0
  267. package/dist/types/tools/agent-tree.d.ts +36 -0
  268. package/dist/types/tools/ask.d.ts +67 -0
  269. package/dist/types/tools/ast-edit.d.ts +53 -0
  270. package/dist/types/tools/ast-grep.d.ts +52 -0
  271. package/dist/types/tools/autoresearch.d.ts +156 -0
  272. package/dist/types/tools/bash-interactive.d.ts +28 -0
  273. package/dist/types/tools/bash.d.ts +103 -0
  274. package/dist/types/tools/conflict-detect.d.ts +43 -0
  275. package/dist/types/tools/debug.d.ts +60 -0
  276. package/dist/types/tools/default-renderer.d.ts +24 -0
  277. package/dist/types/tools/edit.d.ts +158 -0
  278. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +5 -0
  279. package/dist/types/tools/eval-format/index.d.ts +5 -0
  280. package/dist/types/tools/eval-format/javascript.d.ts +2 -0
  281. package/dist/types/tools/eval-format/python.d.ts +2 -0
  282. package/dist/types/tools/eval.d.ts +94 -0
  283. package/dist/types/tools/fetch.d.ts +42 -0
  284. package/dist/types/tools/gh-format.d.ts +8 -0
  285. package/dist/types/tools/github.d.ts +144 -0
  286. package/dist/types/tools/glob.d.ts +44 -0
  287. package/dist/types/tools/goal.d.ts +42 -0
  288. package/dist/types/tools/grep.d.ts +64 -0
  289. package/dist/types/tools/grouped-file-output.d.ts +71 -0
  290. package/dist/types/tools/hashline-format.d.ts +28 -0
  291. package/dist/types/tools/hub.d.ts +340 -0
  292. package/dist/types/tools/index.d.ts +4 -0
  293. package/dist/types/tools/json-tree.d.ts +59 -0
  294. package/dist/types/tools/line-ranges.d.ts +29 -0
  295. package/dist/types/tools/list-limit.d.ts +15 -0
  296. package/dist/types/tools/lsp.d.ts +72 -0
  297. package/dist/types/tools/match-line-format.d.ts +11 -0
  298. package/dist/types/tools/mcp.d.ts +81 -0
  299. package/dist/types/tools/memory.d.ts +67 -0
  300. package/dist/types/tools/output-meta.d.ts +151 -0
  301. package/dist/types/tools/read.d.ts +139 -0
  302. package/dist/types/tools/renderer.d.ts +98 -0
  303. package/dist/types/tools/report-tool-issue.d.ts +8 -0
  304. package/dist/types/tools/resolve.d.ts +47 -0
  305. package/dist/types/tools/streaming-output.d.ts +282 -0
  306. package/dist/types/tools/subprocess.d.ts +24 -0
  307. package/dist/types/tools/task-repair-args.d.ts +53 -0
  308. package/dist/types/tools/task-yield-assembly.d.ts +20 -0
  309. package/dist/types/tools/task.d.ts +425 -0
  310. package/dist/types/tools/terminal-output.d.ts +8 -0
  311. package/dist/types/tools/think.d.ts +13 -0
  312. package/dist/types/tools/todo.d.ts +118 -0
  313. package/dist/types/tools/tool-errors.d.ts +7 -0
  314. package/dist/types/tools/vibe.d.ts +120 -0
  315. package/dist/types/tools/web-search.d.ts +195 -0
  316. package/dist/types/tools/write.d.ts +76 -0
  317. package/dist/types/tools/xd-url.d.ts +13 -0
  318. package/dist/types/tools/xdev.d.ts +52 -0
  319. package/dist/types/tui.d.ts +18 -0
  320. package/dist/types/utils.d.ts +2 -0
  321. package/package.json +37 -3
  322. package/src/app-keybindings.ts +760 -0
  323. package/src/apps/autoresearch-dashboard.ts +500 -0
  324. package/src/apps/autoresearch-data.ts +64 -0
  325. package/src/apps/cleanse-board.ts +341 -0
  326. package/src/apps/cleanse-picker.ts +57 -0
  327. package/src/apps/debug/log-formatting.ts +53 -0
  328. package/src/apps/debug/log-viewer.ts +868 -0
  329. package/src/apps/debug/protocol-probe.ts +267 -0
  330. package/src/apps/debug/raw-sse-buffer.ts +449 -0
  331. package/src/apps/debug/raw-sse.ts +282 -0
  332. package/src/apps/debug/terminal-info.ts +115 -0
  333. package/src/apps/debug/viewer-frame.ts +174 -0
  334. package/src/apps/git/avatar.ts +52 -0
  335. package/src/apps/git/colors.ts +109 -0
  336. package/src/apps/git/diff-pane.ts +1512 -0
  337. package/src/apps/git/git-tui.ts +889 -0
  338. package/src/apps/git/sidebar.ts +1204 -0
  339. package/src/apps/git/state.ts +113 -0
  340. package/src/apps/if-bench-board.ts +324 -0
  341. package/src/apps/live-visualizer.ts +238 -0
  342. package/src/apps/ps-data.ts +116 -0
  343. package/src/apps/ps-top.ts +422 -0
  344. package/src/apps/session-picker.ts +95 -0
  345. package/src/apps/setup-model-picker.ts +21 -0
  346. package/src/apps/standalone-picker.ts +111 -0
  347. package/src/autocomplete.ts +9 -13
  348. package/src/chat/advisor-message.ts +175 -0
  349. package/src/chat/assistant-message.ts +1214 -0
  350. package/src/chat/background-tan-message.ts +35 -0
  351. package/src/chat/bash-execution.ts +394 -0
  352. package/src/chat/cache-invalidation-marker.ts +94 -0
  353. package/src/chat/chat-transcript-builder.ts +593 -0
  354. package/src/chat/collab-prompt-message.ts +34 -0
  355. package/src/chat/compaction-summary-message.ts +261 -0
  356. package/src/chat/custom-message.ts +23 -0
  357. package/src/chat/display-preferences.ts +24 -0
  358. package/src/chat/eval-execution.ts +172 -0
  359. package/src/chat/execution-shared.ts +113 -0
  360. package/src/chat/extension-types.ts +46 -0
  361. package/src/chat/hook-message.ts +20 -0
  362. package/src/chat/image-loading.ts +122 -0
  363. package/src/chat/late-diagnostics-message.ts +92 -0
  364. package/src/chat/messages.ts +305 -0
  365. package/src/chat/reaction.ts +65 -0
  366. package/src/chat/read-target.ts +7 -0
  367. package/src/chat/read-tool-group.ts +873 -0
  368. package/src/chat/served-model-marker.ts +91 -0
  369. package/src/chat/skill-message.ts +220 -0
  370. package/src/chat/skill-title-input.ts +41 -0
  371. package/src/chat/stripped-tool-calls-placeholder.ts +35 -0
  372. package/src/chat/thinking-display.ts +312 -0
  373. package/src/chat/todo-reminder.ts +33 -0
  374. package/src/chat/tool-execution.ts +1395 -0
  375. package/src/chat/transcript-browser.ts +268 -0
  376. package/src/chat/transcript-entry.ts +96 -0
  377. package/src/chat/transcript-outline.ts +260 -0
  378. package/src/chat/transcript-render-helpers.ts +315 -0
  379. package/src/chat/ttsr-notification.ts +123 -0
  380. package/src/chat/user-message.ts +287 -0
  381. package/src/chrome/chat-block.ts +110 -0
  382. package/src/chrome/collab-qrcode.ts +74 -0
  383. package/src/chrome/context-thresholds.ts +85 -0
  384. package/src/chrome/countdown-timer.ts +74 -0
  385. package/src/chrome/diff.ts +266 -0
  386. package/src/chrome/dynamic-border.ts +36 -0
  387. package/src/chrome/error-block.ts +63 -0
  388. package/src/chrome/form-theme.ts +10 -0
  389. package/src/chrome/format.ts +66 -0
  390. package/src/chrome/index.ts +21 -0
  391. package/src/chrome/keybinding-hints.ts +55 -0
  392. package/src/chrome/live-board.ts +123 -0
  393. package/src/chrome/local-date.ts +20 -0
  394. package/src/chrome/message-divider.ts +51 -0
  395. package/src/chrome/message-frame.ts +164 -0
  396. package/src/chrome/message-notice.ts +103 -0
  397. package/src/chrome/overlay-box.ts +301 -0
  398. package/src/chrome/qrcode.ts +539 -0
  399. package/src/chrome/segment-track.ts +86 -0
  400. package/src/chrome/select-list-mouse-routing.ts +11 -0
  401. package/src/chrome/selector-helpers.ts +119 -0
  402. package/src/chrome/shared.ts +27 -0
  403. package/src/chrome/streaming-panel.ts +72 -0
  404. package/src/chrome/tool-activity.ts +44 -0
  405. package/src/chrome/transcript-container.ts +902 -0
  406. package/src/chrome/transcript-status.ts +27 -0
  407. package/src/chrome/visual-truncate.ts +70 -0
  408. package/src/components/disclosure.ts +235 -0
  409. package/src/components/editor.ts +10 -12
  410. package/src/components/form.ts +566 -0
  411. package/src/components/input.ts +6 -27
  412. package/src/components/key-value-list.ts +88 -0
  413. package/src/components/layout/geometry.ts +202 -0
  414. package/src/components/layout/row.ts +283 -0
  415. package/src/components/layout/split-pane.ts +349 -0
  416. package/src/components/layout/stack.ts +261 -0
  417. package/src/components/markdown.ts +16 -12
  418. package/src/components/menu-selection.ts +248 -0
  419. package/src/components/metric.ts +132 -0
  420. package/src/components/progress-bar.ts +140 -0
  421. package/src/components/scroll-view.ts +372 -70
  422. package/src/components/scroll-viewport.ts +154 -0
  423. package/src/components/section.ts +77 -0
  424. package/src/components/select-list.ts +238 -159
  425. package/src/components/settings-list.ts +103 -80
  426. package/src/components/table.ts +127 -0
  427. package/src/components/tree-view.ts +481 -0
  428. package/src/components/wizard-step.ts +229 -0
  429. package/src/deccara.ts +1 -3
  430. package/src/hotkeys-markdown.ts +100 -0
  431. package/src/index.ts +18 -0
  432. package/src/keybinding-matchers.ts +99 -0
  433. package/src/keys.ts +2 -2
  434. package/src/lang-from-path.ts +252 -0
  435. package/src/overlays/advisor-config.ts +761 -0
  436. package/src/overlays/agent-activity.ts +113 -0
  437. package/src/overlays/agent-hub-projection.ts +250 -0
  438. package/src/overlays/agent-hub-renderer.ts +281 -0
  439. package/src/overlays/agent-hub-types.ts +74 -0
  440. package/src/overlays/agent-hub.ts +1562 -0
  441. package/src/overlays/agent-transcript-viewer.ts +669 -0
  442. package/src/overlays/agents-hub.ts +1203 -0
  443. package/src/overlays/ask-dialog.ts +1202 -0
  444. package/src/overlays/bordered-loader.ts +41 -0
  445. package/src/overlays/btw-history-panel.ts +591 -0
  446. package/src/overlays/btw-history.ts +33 -0
  447. package/src/overlays/btw-panel.ts +167 -0
  448. package/src/overlays/cleanse-panel.ts +167 -0
  449. package/src/overlays/codex-reset-fireworks.ts +375 -0
  450. package/src/overlays/composer-shape-preview.ts +137 -0
  451. package/src/overlays/composer-shape-registry.ts +82 -0
  452. package/src/overlays/copy-selector.ts +618 -0
  453. package/src/overlays/copy-targets.ts +210 -0
  454. package/src/overlays/error-banner.ts +39 -0
  455. package/src/overlays/extensions/display-text.ts +39 -0
  456. package/src/overlays/extensions/extension-dashboard.ts +645 -0
  457. package/src/overlays/extensions/extension-list.ts +667 -0
  458. package/src/overlays/extensions/inspector-model.ts +484 -0
  459. package/src/overlays/extensions/inspector-panel.ts +669 -0
  460. package/src/overlays/extensions/live-tool-session.ts +67 -0
  461. package/src/overlays/extensions/mcp-runtime.ts +289 -0
  462. package/src/overlays/extensions/state-manager.ts +337 -0
  463. package/src/overlays/extensions/types.ts +208 -0
  464. package/src/overlays/history-search.ts +268 -0
  465. package/src/overlays/hook-editor.ts +272 -0
  466. package/src/overlays/hook-input.ts +84 -0
  467. package/src/overlays/hook-selector.ts +635 -0
  468. package/src/overlays/hub-frame.ts +262 -0
  469. package/src/overlays/login-dialog.ts +231 -0
  470. package/src/overlays/logout-account-selector.ts +115 -0
  471. package/src/overlays/mcp-add-wizard.ts +1436 -0
  472. package/src/overlays/model-browser.ts +1176 -0
  473. package/src/overlays/model-hub.ts +2279 -0
  474. package/src/overlays/model-picker.ts +285 -0
  475. package/src/overlays/model-selector.ts +107 -0
  476. package/src/overlays/move-overlay.ts +197 -0
  477. package/src/overlays/oauth-selector.ts +446 -0
  478. package/src/overlays/omfg-panel.ts +136 -0
  479. package/src/overlays/pause-screen.ts +198 -0
  480. package/src/overlays/plan-review-overlay.ts +1227 -0
  481. package/src/overlays/plan-save-overlay.ts +74 -0
  482. package/src/overlays/plan-toc.ts +139 -0
  483. package/src/overlays/plugin-selector.ts +97 -0
  484. package/src/overlays/plugin-settings.ts +839 -0
  485. package/src/overlays/queue-mode-selector.ts +55 -0
  486. package/src/overlays/reset-usage-selector.ts +170 -0
  487. package/src/overlays/rewind-selector.ts +499 -0
  488. package/src/overlays/running-subagent-badge.ts +19 -0
  489. package/src/overlays/session-account-selector.ts +68 -0
  490. package/src/overlays/session-info-overlay.ts +96 -0
  491. package/src/overlays/session-observer-registry.ts +289 -0
  492. package/src/overlays/session-selector.ts +1137 -0
  493. package/src/overlays/settings-defs.ts +315 -0
  494. package/src/overlays/settings-selector.ts +1415 -0
  495. package/src/overlays/show-images-selector.ts +44 -0
  496. package/src/overlays/snapcompact-shape-preview-doc.md +14 -0
  497. package/src/overlays/snapcompact-shape-preview.ts +192 -0
  498. package/src/overlays/theme-selector.ts +62 -0
  499. package/src/overlays/thinking-selector.ts +51 -0
  500. package/src/overlays/tiny-title-download-progress.ts +102 -0
  501. package/src/overlays/tree-selector.ts +1121 -0
  502. package/src/overlays/usage-dashboard.ts +629 -0
  503. package/src/overlays/usage-display.ts +45 -0
  504. package/src/overlays/usage-row.ts +118 -0
  505. package/src/prompt/attachment-chips.ts +201 -0
  506. package/src/prompt/composer-attachments.ts +280 -0
  507. package/src/prompt/composer-cache.ts +304 -0
  508. package/src/prompt/composer.ts +934 -0
  509. package/src/prompt/custom-editor.ts +1392 -0
  510. package/src/prompt/data/emojis.json +1 -0
  511. package/src/prompt/editor-top-gap.ts +28 -0
  512. package/src/prompt/emoji-autocomplete.ts +285 -0
  513. package/src/prompt/github-ref-autocomplete.ts +75 -0
  514. package/src/prompt/gradient-highlight.ts +101 -0
  515. package/src/prompt/image-format.ts +28 -0
  516. package/src/prompt/image-references.ts +102 -0
  517. package/src/prompt/interactive-context-helpers.ts +126 -0
  518. package/src/prompt/internal-url-autocomplete.ts +155 -0
  519. package/src/prompt/macos-spelling.ts +456 -0
  520. package/src/prompt/magic-keyword-boundary.ts +23 -0
  521. package/src/prompt/magic-keywords.ts +42 -0
  522. package/src/prompt/markdown-prose.ts +245 -0
  523. package/src/prompt/model-mention-autocomplete.ts +94 -0
  524. package/src/prompt/model-mention-syntax.ts +41 -0
  525. package/src/prompt/orchestrate.ts +26 -0
  526. package/src/prompt/prompt-action-autocomplete.ts +337 -0
  527. package/src/prompt/queue-input.ts +132 -0
  528. package/src/prompt/skill-tokens.ts +36 -0
  529. package/src/prompt/tips.txt +27 -0
  530. package/src/prompt/tools-markdown.ts +29 -0
  531. package/src/prompt/ultrathink.ts +26 -0
  532. package/src/prompt/usage-amounts.ts +99 -0
  533. package/src/prompt/video.ts +47 -0
  534. package/src/prompt/welcome.ts +605 -0
  535. package/src/prompt/workflow.ts +26 -0
  536. package/src/render/code-cell.ts +244 -0
  537. package/src/render/file-list.ts +58 -0
  538. package/src/render/hyperlink.ts +192 -0
  539. package/src/render/index.ts +17 -0
  540. package/src/render/output-block.ts +267 -0
  541. package/src/render/output-pane.ts +299 -0
  542. package/src/render/render-utils.ts +1182 -0
  543. package/src/render/sixel.ts +69 -0
  544. package/src/render/status-line.ts +55 -0
  545. package/src/render/tool-card.ts +272 -0
  546. package/src/render/tree-list.ts +188 -0
  547. package/src/render/types.ts +19 -0
  548. package/src/render/utils.ts +109 -0
  549. package/src/render/width-aware-text.ts +60 -0
  550. package/src/setup/lazy.ts +17 -0
  551. package/src/setup/scenes/composer.ts +127 -0
  552. package/src/setup/scenes/glyph.ts +120 -0
  553. package/src/setup/scenes/model.ts +138 -0
  554. package/src/setup/scenes/outro.ts +24 -0
  555. package/src/setup/scenes/providers.ts +107 -0
  556. package/src/setup/scenes/sign-in.ts +373 -0
  557. package/src/setup/scenes/splash.ts +192 -0
  558. package/src/setup/scenes/theme.ts +343 -0
  559. package/src/setup/scenes/types.ts +111 -0
  560. package/src/setup/scenes/web-search.ts +165 -0
  561. package/src/setup/setup-version.ts +11 -0
  562. package/src/setup/startup-splash.ts +107 -0
  563. package/src/setup/wizard-overlay.ts +319 -0
  564. package/src/setup/wizard.ts +101 -0
  565. package/src/status-line/component.ts +3003 -0
  566. package/src/status-line/context-usage.ts +688 -0
  567. package/src/status-line/footer.ts +323 -0
  568. package/src/status-line/git-utils.ts +42 -0
  569. package/src/status-line/host.ts +97 -0
  570. package/src/status-line/index.ts +6 -0
  571. package/src/status-line/loop.ts +28 -0
  572. package/src/status-line/metrics.ts +94 -0
  573. package/src/status-line/presets.ts +107 -0
  574. package/src/status-line/schema.ts +57 -0
  575. package/src/status-line/segments.ts +934 -0
  576. package/src/status-line/separators.ts +55 -0
  577. package/src/status-line/types.ts +227 -0
  578. package/src/stdin-buffer.ts +4 -2
  579. package/src/theme/color.ts +95 -0
  580. package/src/theme/dark.json +95 -0
  581. package/src/theme/defaults/alabaster.json +93 -0
  582. package/src/theme/defaults/amethyst.json +96 -0
  583. package/src/theme/defaults/anthracite.json +93 -0
  584. package/src/theme/defaults/basalt.json +91 -0
  585. package/src/theme/defaults/birch.json +95 -0
  586. package/src/theme/defaults/dark-abyss.json +91 -0
  587. package/src/theme/defaults/dark-arctic.json +104 -0
  588. package/src/theme/defaults/dark-aurora.json +95 -0
  589. package/src/theme/defaults/dark-catppuccin.json +107 -0
  590. package/src/theme/defaults/dark-cavern.json +91 -0
  591. package/src/theme/defaults/dark-celestial.json +106 -0
  592. package/src/theme/defaults/dark-copper.json +95 -0
  593. package/src/theme/defaults/dark-cosmos.json +90 -0
  594. package/src/theme/defaults/dark-cyberpunk.json +102 -0
  595. package/src/theme/defaults/dark-dracula.json +98 -0
  596. package/src/theme/defaults/dark-eclipse.json +91 -0
  597. package/src/theme/defaults/dark-ember.json +95 -0
  598. package/src/theme/defaults/dark-equinox.json +90 -0
  599. package/src/theme/defaults/dark-forest.json +96 -0
  600. package/src/theme/defaults/dark-github.json +105 -0
  601. package/src/theme/defaults/dark-gruvbox.json +112 -0
  602. package/src/theme/defaults/dark-lavender.json +95 -0
  603. package/src/theme/defaults/dark-lunar.json +89 -0
  604. package/src/theme/defaults/dark-midnight.json +95 -0
  605. package/src/theme/defaults/dark-monochrome.json +94 -0
  606. package/src/theme/defaults/dark-monokai.json +98 -0
  607. package/src/theme/defaults/dark-nebula.json +90 -0
  608. package/src/theme/defaults/dark-nord.json +98 -0
  609. package/src/theme/defaults/dark-ocean.json +101 -0
  610. package/src/theme/defaults/dark-one.json +100 -0
  611. package/src/theme/defaults/dark-poimandres.json +115 -0
  612. package/src/theme/defaults/dark-rainforest.json +91 -0
  613. package/src/theme/defaults/dark-reef.json +91 -0
  614. package/src/theme/defaults/dark-retro.json +92 -0
  615. package/src/theme/defaults/dark-rose-pine.json +96 -0
  616. package/src/theme/defaults/dark-sakura.json +95 -0
  617. package/src/theme/defaults/dark-slate.json +95 -0
  618. package/src/theme/defaults/dark-solarized.json +97 -0
  619. package/src/theme/defaults/dark-solstice.json +90 -0
  620. package/src/theme/defaults/dark-starfall.json +91 -0
  621. package/src/theme/defaults/dark-sunset.json +99 -0
  622. package/src/theme/defaults/dark-swamp.json +90 -0
  623. package/src/theme/defaults/dark-synthwave.json +103 -0
  624. package/src/theme/defaults/dark-taiga.json +91 -0
  625. package/src/theme/defaults/dark-terminal.json +95 -0
  626. package/src/theme/defaults/dark-tokyo-night.json +101 -0
  627. package/src/theme/defaults/dark-tundra.json +91 -0
  628. package/src/theme/defaults/dark-twilight.json +91 -0
  629. package/src/theme/defaults/dark-volcanic.json +91 -0
  630. package/src/theme/defaults/graphite.json +92 -0
  631. package/src/theme/defaults/index.ts +201 -0
  632. package/src/theme/defaults/light-arctic.json +107 -0
  633. package/src/theme/defaults/light-aurora-day.json +91 -0
  634. package/src/theme/defaults/light-canyon.json +91 -0
  635. package/src/theme/defaults/light-catppuccin.json +106 -0
  636. package/src/theme/defaults/light-cirrus.json +90 -0
  637. package/src/theme/defaults/light-coral.json +95 -0
  638. package/src/theme/defaults/light-cyberpunk.json +96 -0
  639. package/src/theme/defaults/light-dawn.json +90 -0
  640. package/src/theme/defaults/light-dunes.json +91 -0
  641. package/src/theme/defaults/light-eucalyptus.json +95 -0
  642. package/src/theme/defaults/light-forest.json +100 -0
  643. package/src/theme/defaults/light-frost.json +95 -0
  644. package/src/theme/defaults/light-github.json +115 -0
  645. package/src/theme/defaults/light-glacier.json +91 -0
  646. package/src/theme/defaults/light-gruvbox.json +108 -0
  647. package/src/theme/defaults/light-haze.json +90 -0
  648. package/src/theme/defaults/light-honeycomb.json +95 -0
  649. package/src/theme/defaults/light-lagoon.json +91 -0
  650. package/src/theme/defaults/light-lavender.json +95 -0
  651. package/src/theme/defaults/light-meadow.json +91 -0
  652. package/src/theme/defaults/light-mint.json +95 -0
  653. package/src/theme/defaults/light-monochrome.json +101 -0
  654. package/src/theme/defaults/light-ocean.json +99 -0
  655. package/src/theme/defaults/light-one.json +99 -0
  656. package/src/theme/defaults/light-opal.json +91 -0
  657. package/src/theme/defaults/light-orchard.json +91 -0
  658. package/src/theme/defaults/light-paper.json +95 -0
  659. package/src/theme/defaults/light-poimandres.json +115 -0
  660. package/src/theme/defaults/light-prism.json +90 -0
  661. package/src/theme/defaults/light-retro.json +98 -0
  662. package/src/theme/defaults/light-sand.json +95 -0
  663. package/src/theme/defaults/light-savanna.json +91 -0
  664. package/src/theme/defaults/light-solarized.json +102 -0
  665. package/src/theme/defaults/light-soleil.json +90 -0
  666. package/src/theme/defaults/light-sunset.json +99 -0
  667. package/src/theme/defaults/light-synthwave.json +98 -0
  668. package/src/theme/defaults/light-tokyo-night.json +111 -0
  669. package/src/theme/defaults/light-wetland.json +91 -0
  670. package/src/theme/defaults/light-zenith.json +89 -0
  671. package/src/theme/defaults/limestone.json +94 -0
  672. package/src/theme/defaults/mahogany.json +97 -0
  673. package/src/theme/defaults/marble.json +93 -0
  674. package/src/theme/defaults/obsidian.json +91 -0
  675. package/src/theme/defaults/onyx.json +91 -0
  676. package/src/theme/defaults/pearl.json +93 -0
  677. package/src/theme/defaults/porcelain.json +91 -0
  678. package/src/theme/defaults/quartz.json +96 -0
  679. package/src/theme/defaults/sandstone.json +95 -0
  680. package/src/theme/defaults/titanium.json +90 -0
  681. package/src/theme/index.ts +5 -0
  682. package/src/theme/light.json +93 -0
  683. package/src/theme/loader.ts +200 -0
  684. package/src/theme/mermaid-cache.ts +92 -0
  685. package/src/theme/schema-validation.ts +119 -0
  686. package/src/theme/schema.ts +168 -0
  687. package/src/theme/session-color.ts +253 -0
  688. package/src/theme/shimmer.ts +307 -0
  689. package/src/theme/symbols.ts +1433 -0
  690. package/src/theme/theme-class.ts +787 -0
  691. package/src/theme/theme-schema.json +463 -0
  692. package/src/theme/theme.ts +810 -0
  693. package/src/theme/tui-adapters.ts +339 -0
  694. package/src/thinking.ts +380 -0
  695. package/src/tools/agent-tree.ts +128 -0
  696. package/src/tools/ask.ts +482 -0
  697. package/src/tools/ast-edit.ts +210 -0
  698. package/src/tools/ast-grep.ts +171 -0
  699. package/src/tools/autoresearch.ts +287 -0
  700. package/src/tools/bash-interactive.ts +295 -0
  701. package/src/tools/bash.ts +468 -0
  702. package/src/tools/conflict-detect.ts +77 -0
  703. package/src/tools/debug.ts +167 -0
  704. package/src/tools/default-renderer.ts +172 -0
  705. package/src/tools/edit.ts +1258 -0
  706. package/src/tools/eval-format/code-mode-declarations.ts +61 -0
  707. package/src/tools/eval-format/index.ts +16 -0
  708. package/src/tools/eval-format/javascript.ts +952 -0
  709. package/src/tools/eval-format/python.ts +544 -0
  710. package/src/tools/eval.ts +835 -0
  711. package/src/tools/fetch.ts +226 -0
  712. package/src/tools/gh-format.ts +18 -0
  713. package/src/tools/github.ts +686 -0
  714. package/src/tools/glob.ts +213 -0
  715. package/src/tools/goal.ts +153 -0
  716. package/src/tools/grep.ts +385 -0
  717. package/src/tools/grouped-file-output.ts +211 -0
  718. package/src/tools/hashline-format.ts +59 -0
  719. package/src/tools/hub.ts +1488 -0
  720. package/src/tools/index.ts +71 -0
  721. package/src/tools/json-tree.ts +388 -0
  722. package/src/tools/line-ranges.ts +98 -0
  723. package/src/tools/list-limit.ts +43 -0
  724. package/src/tools/lsp.ts +702 -0
  725. package/src/tools/match-line-format.ts +20 -0
  726. package/src/tools/mcp.ts +309 -0
  727. package/src/tools/memory.ts +220 -0
  728. package/src/tools/output-meta.ts +367 -0
  729. package/src/tools/read.ts +628 -0
  730. package/src/tools/renderer.ts +107 -0
  731. package/src/tools/report-tool-issue.ts +13 -0
  732. package/src/tools/resolve.ts +139 -0
  733. package/src/tools/streaming-output.ts +1576 -0
  734. package/src/tools/subprocess.ts +39 -0
  735. package/src/tools/task-repair-args.ts +118 -0
  736. package/src/tools/task-yield-assembly.ts +147 -0
  737. package/src/tools/task.ts +2110 -0
  738. package/src/tools/terminal-output.ts +153 -0
  739. package/src/tools/think.ts +28 -0
  740. package/src/tools/todo.ts +580 -0
  741. package/src/tools/tool-errors.ts +15 -0
  742. package/src/tools/vibe.ts +420 -0
  743. package/src/tools/web-search.ts +409 -0
  744. package/src/tools/write.ts +511 -0
  745. package/src/tools/xd-url.ts +28 -0
  746. package/src/tools/xdev.ts +201 -0
  747. package/src/tui.ts +57 -4
  748. package/src/utils.ts +8 -0
@@ -0,0 +1,1182 @@
1
+ /**
2
+ * Shared utilities and constants for tool renderers.
3
+ *
4
+ * Provides consistent formatting, truncation, and display patterns across all
5
+ * tool renderers to ensure a unified TUI experience.
6
+ */
7
+
8
+ import * as os from "node:os";
9
+ import * as path from "node:path";
10
+ import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
11
+ import type { Ellipsis } from "@oh-my-pi/pi-natives";
12
+ import { pluralize, sanitizeText } from "@oh-my-pi/pi-utils";
13
+ import { formatKeyHints, type KeyId } from "../app-keybindings";
14
+ import { getKeybindings } from "../keybindings";
15
+ import type { Theme } from "../theme/theme";
16
+ import type { Component } from "../tui";
17
+ import { replaceTabs, sliceByColumn, truncateToWidth, visibleWidth } from "../utils";
18
+ import { Hasher } from "./utils";
19
+
20
+ export { Ellipsis } from "@oh-my-pi/pi-natives";
21
+ export { replaceTabs, truncateToWidth, wrapTextWithAnsi } from "../utils";
22
+
23
+ /** A thinking selector configured as a concrete level or automatic selection. */
24
+ export type ConfiguredThinkingLevel = ThinkingLevel | "auto";
25
+
26
+ /**
27
+ * Normalize stray carriage returns in model-authored display text. Some models
28
+ * (observed with GLM via OpenRouter) degenerate into injecting `\r` runs between
29
+ * words inside JSON string values; CommonMark treats a lone `\r` as a line
30
+ * ending, which splatters the text one word per row. CRLF becomes LF, CR runs
31
+ * collapse to a single space — word separators in prose, one indent unit in
32
+ * mangled code previews.
33
+ */
34
+ export function sanitizeCarriageReturns(text: string): string {
35
+ if (!text.includes("\r")) return text;
36
+ return text.replaceAll("\r\n", "\n").replace(/\r+/g, " ");
37
+ }
38
+
39
+ /**
40
+ * Sanitize raw ask option labels into unique, action-safe display copies.
41
+ * Degenerate input can sanitize alike (`Retry\rnow`/`Retry now`) or match a
42
+ * runtime action row (`Other (type your own)`); both would answer the wrong
43
+ * row, so colliding entries take a numeric suffix. Order and length are
44
+ * preserved, so indices still align with the original labels for mapping
45
+ * answers and dialog state back. Every ask race participant (local dialog,
46
+ * guest selector) must call this with the same `reservedLabels` so a
47
+ * question renders identically wherever it is answered.
48
+ */
49
+ export function disambiguateDisplayLabels(rawLabels: string[], reservedLabels: readonly string[]): string[] {
50
+ const taken = new Set<string>(reservedLabels);
51
+ return rawLabels.map(raw => {
52
+ const base = sanitizeCarriageReturns(raw);
53
+ let candidate = base;
54
+ for (let suffix = 2; taken.has(candidate); suffix++) candidate = `${base} (${suffix})`;
55
+ taken.add(candidate);
56
+ return candidate;
57
+ });
58
+ }
59
+
60
+ // =============================================================================
61
+ // Standardized Display Constants
62
+ // =============================================================================
63
+
64
+ /** Defaults for the host's tui.maxInlineImageColumns and tui.maxInlineImageRows settings. */
65
+ let inlineImageMaxColumns = 100;
66
+ let inlineImageMaxRows = 20;
67
+
68
+ /** Set the maximum inline image width in terminal columns; zero leaves it uncapped. */
69
+ export function setInlineImageMaxColumns(maxColumns: number): void {
70
+ inlineImageMaxColumns = maxColumns;
71
+ }
72
+
73
+ /** Set the maximum inline image height in terminal rows; zero uses the viewport limit. */
74
+ export function setInlineImageMaxRows(maxRows: number): void {
75
+ inlineImageMaxRows = maxRows;
76
+ }
77
+
78
+ /** Resolve inline image dimension caps from the configured limits and viewport. */
79
+ export function resolveImageOptions(): { maxWidthCells: number; maxHeightCells?: number } {
80
+ const maxWidthCells = inlineImageMaxColumns;
81
+ const rowSetting = Math.max(0, inlineImageMaxRows);
82
+ const viewportRows = process.stdout.rows;
83
+ const viewportFraction = viewportRows ? Math.floor(viewportRows * 0.6) : 0;
84
+ let maxHeightCells: number | undefined;
85
+ if (rowSetting === 0) {
86
+ // No explicit cap — use viewport fraction as safety bound
87
+ maxHeightCells = viewportFraction || undefined;
88
+ } else if (viewportFraction > 0) {
89
+ maxHeightCells = Math.min(rowSetting, viewportFraction);
90
+ } else {
91
+ // Viewport size unknown (transitional state) — honor explicit setting
92
+ maxHeightCells = rowSetting;
93
+ }
94
+ return { maxWidthCells, maxHeightCells };
95
+ }
96
+
97
+ /** Preview limits for collapsed/expanded views */
98
+ export const PREVIEW_LIMITS = {
99
+ /** Lines shown in collapsed view */
100
+ COLLAPSED_LINES: 3,
101
+ /** Lines shown in expanded view */
102
+ EXPANDED_LINES: 12,
103
+ /** Items (files, results) shown in collapsed view */
104
+ COLLAPSED_ITEMS: 8,
105
+ /** Output preview lines in collapsed view */
106
+ OUTPUT_COLLAPSED: 3,
107
+ /** Output preview lines in expanded view */
108
+ OUTPUT_EXPANDED: 10,
109
+ /** Computer script lines shown in collapsed view */
110
+ COMPUTER_CODE_COLLAPSED: 10,
111
+ /** Max hunks shown when collapsed (edit tool) */
112
+ DIFF_COLLAPSED_HUNKS: 8,
113
+ /** Max diff lines shown when collapsed (edit tool) */
114
+ DIFF_COLLAPSED_LINES: 40,
115
+ } as const;
116
+
117
+ /** Default number of terminal output rows shown before expansion. */
118
+ export const DEFAULT_TERMINAL_PREVIEW_LINES = 10;
119
+
120
+ /** Match a Markdown fenced-code opener and capture its indentation and marker. */
121
+ export const FENCE_RE = /^( {0,3})([`~]{3,})/;
122
+
123
+ /** Shared empty link-target lookup for renderers without interactive links. */
124
+ export const EMPTY_LINK_TARGETS: ReadonlyMap<string, string> = new Map();
125
+
126
+ /** Default display width reserved for a resolved model badge. */
127
+ export const FEED_MODEL_BADGE_WIDTH = 30;
128
+
129
+ let feedModelBadgeEnabled = false;
130
+
131
+ /** Set whether feed rows display their resolved model badge. */
132
+ export function setFeedModelBadgeEnabled(enabled: boolean): void {
133
+ feedModelBadgeEnabled = enabled;
134
+ }
135
+
136
+ /** Return whether resolved model badges are enabled. */
137
+ export function isFeedModelBadgeEnabled(): boolean {
138
+ return feedModelBadgeEnabled;
139
+ }
140
+
141
+ /** Compact glyph for a configured thinking level; empty for `inherit` (nothing to show). */
142
+ export function thinkingLevelGlyph(level: ConfiguredThinkingLevel, uiTheme: Theme): string {
143
+ if (level === ThinkingLevel.Inherit) return "";
144
+ if (level === ThinkingLevel.Off) return uiTheme.status.disabled;
145
+ const symbol = uiTheme.thinking[level === "auto" ? "autoPending" : level];
146
+ if (typeof symbol !== "string") return "";
147
+ const space = symbol.indexOf(" ");
148
+ return space < 0 ? symbol : symbol.slice(0, space);
149
+ }
150
+
151
+ /**
152
+ * Compact feed-row prefix: explicit thinking glyph, sanitized model identity,
153
+ * then advisor eye. Keep fitting icons if no model fits; preserve literal identity suffixes.
154
+ */
155
+ export function formatFeedModelBadge(
156
+ modelIdentity: string | undefined,
157
+ thinkingLevel: ConfiguredThinkingLevel | undefined,
158
+ advisor: boolean | undefined,
159
+ uiTheme: Theme,
160
+ maxWidth = FEED_MODEL_BADGE_WIDTH,
161
+ ): string {
162
+ if (!modelIdentity) return "";
163
+ const width = Math.max(0, Math.floor(maxWidth));
164
+ const clean = sanitizeText(modelIdentity).replace(/\s+/g, " ").trim();
165
+ if (!clean || !(width > 0)) return "";
166
+ const glyph = thinkingLevel !== undefined ? thinkingLevelGlyph(thinkingLevel, uiTheme) : "";
167
+ const advisorIcon = advisor === true ? uiTheme.icon.advisor : "";
168
+ const prefix = glyph ? `${glyph} ` : "";
169
+ const suffix = advisorIcon ? ` ${advisorIcon}` : "";
170
+ const modelWidth = width - visibleWidth(prefix) - visibleWidth(suffix);
171
+ if (modelWidth < 1) {
172
+ const advisorWidth = visibleWidth(advisorIcon);
173
+ if (advisorIcon && advisorWidth <= width) {
174
+ const thinkingPrefix = glyph && visibleWidth(prefix) + advisorWidth <= width ? prefix : "";
175
+ return uiTheme.fg("accent", thinkingPrefix) + uiTheme.fg("dim", advisorIcon);
176
+ }
177
+ return glyph && visibleWidth(glyph) <= width ? uiTheme.fg("accent", glyph) : "";
178
+ }
179
+ const model = truncateMiddleToWidth(clean, modelWidth);
180
+ return uiTheme.fg("accent", prefix) + uiTheme.fg("dim", `${model}${suffix}`);
181
+ }
182
+
183
+ /** Truncation lengths for different content types */
184
+ export const TRUNCATE_LENGTHS = {
185
+ /** Short titles, labels */
186
+ TITLE: 60,
187
+ /** Medium-length content (messages, previews) */
188
+ CONTENT: 80,
189
+ /** Longer content (code, explanations) */
190
+ LONG: 100,
191
+ /** Full line content */
192
+ LINE: 110,
193
+ /** Very short (task previews, badges) */
194
+ SHORT: 40,
195
+ /** Idle recap status line (~40-word LLM reply) */
196
+ RECAP: 280,
197
+ } as const;
198
+
199
+ /** Keybinding action that toggles tool-output expansion. */
200
+ const EXPAND_ACTION = "app.tools.expand";
201
+ /** Fallback key when no binding is resolvable (e.g. outside an interactive session). */
202
+ const DEFAULT_EXPAND_KEY: KeyId = "ctrl+o";
203
+
204
+ /** Human-readable key currently bound to tool-output expansion, e.g. `Ctrl+O`. */
205
+ export function expandKeyHint(): string {
206
+ const keys = getKeybindings().getKeys(EXPAND_ACTION);
207
+ return formatKeyHints(keys.length > 0 ? keys : [DEFAULT_EXPAND_KEY]);
208
+ }
209
+
210
+ // =============================================================================
211
+ // Text Truncation Utilities
212
+ // =============================================================================
213
+ /** Keep both ends of a single-line label without splitting wide characters. */
214
+ function truncateMiddleToWidth(text: string, maxWidth: number): string {
215
+ const width = visibleWidth(text);
216
+ if (width <= maxWidth) return text;
217
+ if (maxWidth <= 1) return maxWidth === 1 ? "…" : "";
218
+ const tailWidth = Math.ceil((maxWidth - 1) / 2);
219
+ const headWidth = maxWidth - 1 - tailWidth;
220
+ return `${sliceByColumn(text, 0, headWidth, true)}…${sliceByColumn(text, width - tailWidth, tailWidth, true)}`;
221
+ }
222
+
223
+ /** Select a leading line window and count the lines hidden after it. */
224
+ export function cappedHeadLines(lines: readonly string[], max: number): { lines: readonly string[]; hidden: number } {
225
+ const count = Math.max(0, Math.min(lines.length, Math.floor(max)));
226
+ return { lines: count === lines.length ? lines : lines.slice(0, count), hidden: lines.length - count };
227
+ }
228
+
229
+ /** Get the first nonblank preview lines, trimmed and width-capped. */
230
+ export function getPreviewLines(text: string, maxLines: number, maxLineLen: number, ellipsis?: Ellipsis): string[] {
231
+ const lines = text.split("\n").filter(l => l.trim());
232
+ return lines.slice(0, maxLines).map(l => truncateToWidth(l.trim(), maxLineLen, ellipsis));
233
+ }
234
+
235
+ /**
236
+ * Collapse a possibly multi-line string into a single line, then truncate it to
237
+ * `maxWidth` display cells. {@link truncateToWidth} alone caps width but
238
+ * newlines are zero-width, so multi-line content (markdown briefs, tool args,
239
+ * provider errors) would otherwise spill a single status row across several
240
+ * visual lines. Whitespace runs collapse to one space, so tabs are handled too.
241
+ */
242
+ export function previewLine(text: string, maxWidth: number, ellipsis?: Ellipsis): string {
243
+ return truncateToWidth(text.replace(/\s+/g, " ").trim(), maxWidth, ellipsis);
244
+ }
245
+
246
+ // =============================================================================
247
+ // URL Utilities
248
+ // =============================================================================
249
+
250
+ /**
251
+ * Extract domain from URL, stripping www. prefix.
252
+ */
253
+ export function getDomain(url: string): string {
254
+ try {
255
+ const u = new URL(url);
256
+ return u.hostname.replace(/^www\./, "");
257
+ } catch {
258
+ return url;
259
+ }
260
+ }
261
+
262
+ // =============================================================================
263
+ // Formatting Utilities
264
+ // =============================================================================
265
+
266
+ export { formatAge, formatBytes, formatCount, formatDuration, formatNumber, pluralize } from "@oh-my-pi/pi-utils";
267
+
268
+ // =============================================================================
269
+ // Theme Helper Utilities
270
+ // =============================================================================
271
+
272
+ /**
273
+ * Get the appropriate status icon with color for a given state.
274
+ * Standardizes status icon usage across all renderers.
275
+ */
276
+ export function formatStatusIcon(status: ToolUIStatus, theme: Theme, spinnerFrame?: number): string {
277
+ switch (status) {
278
+ case "success":
279
+ return theme.styledSymbol("status.success", "success");
280
+ case "done":
281
+ return theme.styledSymbol("status.done", "success");
282
+ case "error":
283
+ return theme.styledSymbol("status.error", "error");
284
+ case "warning":
285
+ return theme.styledSymbol("status.warning", "warning");
286
+ case "info":
287
+ return theme.styledSymbol("status.info", "accent");
288
+ case "pending":
289
+ return theme.styledSymbol("status.pending", "muted");
290
+ case "running":
291
+ if (spinnerFrame !== undefined) {
292
+ const frames = theme.spinnerFrames;
293
+ return frames[spinnerFrame % frames.length];
294
+ }
295
+ return theme.styledSymbol("status.running", "accent");
296
+ case "aborted":
297
+ return theme.styledSymbol("status.aborted", "error");
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Format the expand hint with proper theming.
303
+ * Returns empty string if already expanded or there is nothing more to show.
304
+ */
305
+ export function formatExpandHint(theme: Theme, expanded?: boolean, hasMore?: boolean): string {
306
+ if (expanded) return "";
307
+ if (hasMore === false) return "";
308
+ return theme.fg("dim", wrapBrackets(`${expandKeyHint()}: Expand`, theme));
309
+ }
310
+
311
+ /**
312
+ * Format a badge like [done] or [failed] with brackets and color.
313
+ */
314
+ export function formatBadge(label: string, color: ToolUIColor, theme: Theme): string {
315
+ const left = theme.format.bracketLeft;
316
+ const right = theme.format.bracketRight;
317
+ return theme.fg(color, `${left}${label}${right}`);
318
+ }
319
+
320
+ /**
321
+ * Build a "more items" suffix line for truncated lists.
322
+ * Uses consistent wording pattern.
323
+ */
324
+ export function formatMoreItems(remaining: number, itemType: string): string {
325
+ const safeRemaining = Number.isFinite(remaining) ? remaining : 0;
326
+ return `… ${safeRemaining} more ${pluralize(itemType, safeRemaining)}`;
327
+ }
328
+
329
+ /**
330
+ * Collapsed command/code previews render a tail window sized from the live
331
+ * viewport: terminal rows minus a reserve for the rest of the block (frame,
332
+ * Output section, stats line) and the editor/status area below the
333
+ * transcript. This keeps a volatile streaming block from growing past the
334
+ * viewport and stranding its top, while letting tall terminals show more.
335
+ */
336
+ const PREVIEW_WINDOW_RESERVED_ROWS = 20;
337
+ /** Floor so tiny or unknown viewports still show a useful window. */
338
+ const PREVIEW_WINDOW_MIN_LINES = 6;
339
+ /** Assumed viewport when rows are unknown (non-TTY, tests). */
340
+ const PREVIEW_WINDOW_FALLBACK_ROWS = 30;
341
+
342
+ /** Tail-window height for collapsed command/code previews. */
343
+ export function previewWindowRows(): number {
344
+ const rows = process.stdout.rows || PREVIEW_WINDOW_FALLBACK_ROWS;
345
+ return Math.max(PREVIEW_WINDOW_MIN_LINES, rows - PREVIEW_WINDOW_RESERVED_ROWS);
346
+ }
347
+
348
+ /**
349
+ * Cap a pre-rendered command preview to a viewport-sized tail window: the end
350
+ * of the command stays visible (it is the live edge while args stream) behind
351
+ * an "… N earlier lines" marker on top. The same window applies while
352
+ * streaming and after completion so the block never jumps; only `expanded`
353
+ * (ctrl+o) uncaps it.
354
+ *
355
+ * `prefix` (raw, e.g. a dim tree gutter) is prepended to the marker line so
356
+ * nested previews stay aligned. `expandHint: false` drops the "ctrl+o: Expand"
357
+ * suffix for callers that cap even inside the expanded view (task recent
358
+ * output), where the hint would point the wrong way.
359
+ */
360
+ export function capPreviewLines(
361
+ lines: string[],
362
+ theme: Theme,
363
+ options: { max?: number; expanded?: boolean; prefix?: string; expandHint?: boolean } = {},
364
+ ): string[] {
365
+ if (options.expanded) return lines;
366
+ const max = options.max ?? previewWindowRows();
367
+ if (lines.length <= max) return lines;
368
+ const visible = max <= 1 ? [] : lines.slice(lines.length - (max - 1));
369
+ const hidden = lines.length - visible.length;
370
+ const hint = options.expandHint === false ? "" : formatExpandHint(theme, false, true);
371
+ const marker = `… ${hidden} earlier ${pluralize("line", hidden)}${hint ? ` ${hint}` : ""}`;
372
+ return [`${options.prefix ?? ""}${theme.fg("dim", marker)}`, ...visible];
373
+ }
374
+
375
+ /** Join metadata with the theme separator and muted styling. */
376
+ export function formatMeta(meta: string[], theme: Theme): string {
377
+ return meta.length > 0 ? ` ${theme.fg("muted", meta.join(theme.sep.dot))}` : "";
378
+ }
379
+
380
+ function sanitizeErrorText(message: string | undefined): string {
381
+ const clean = (message ?? "").replace(/^Error:\s*/, "").trim();
382
+ return clean ? replaceTabs(truncateToWidth(clean, TRUNCATE_LENGTHS.LINE)) : "Unknown error";
383
+ }
384
+
385
+ /**
386
+ * Split multi-line tool text into TUI-safe display lines.
387
+ * Splits CRLF/LF, collapses stray `\r` progress overwrites to the final
388
+ * segment (mirroring terminal rendering), and expands tabs — so raw
389
+ * subprocess or fetched output (e.g. Windows ssh emitting CRLF, tab-indented
390
+ * web content) can't corrupt the framed block layout with cursor-moving
391
+ * control characters or tab-stop width mismatches.
392
+ */
393
+ export function sanitizeDisplayLines(text: string): string[] {
394
+ return text.split(/\r?\n/).map(line => {
395
+ const idx = line.lastIndexOf("\r");
396
+ return replaceTabs(sanitizeText(idx < 0 ? line : line.slice(idx + 1)));
397
+ });
398
+ }
399
+
400
+ /** Render a sanitized error message with its status icon. */
401
+ export function formatErrorMessage(message: string | undefined, theme: Theme): string {
402
+ return `${theme.styledSymbol("status.error", "error")} ${theme.fg("error", `Error: ${sanitizeErrorText(message)}`)}`;
403
+ }
404
+
405
+ /**
406
+ * Error message rendered as a subordinate detail line beneath a status header
407
+ * that already carries the error icon (e.g. `✘ Write: <path>`). The header's
408
+ * icon already signals failure, so this omits the redundant error symbol and
409
+ * "Error:" prefix that `formatErrorMessage` adds for standalone single-line
410
+ * errors, indenting two columns to sit under the header title instead.
411
+ */
412
+ export function formatErrorDetail(message: string | undefined, theme: Theme): string {
413
+ return ` ${theme.fg("error", sanitizeErrorText(message))}`;
414
+ }
415
+
416
+ /** Render a muted empty-state message with a warning icon. */
417
+ export function formatEmptyMessage(message: string, theme: Theme): string {
418
+ return `${theme.styledSymbol("status.warning", "warning")} ${theme.fg("muted", message)}`;
419
+ }
420
+
421
+ // =============================================================================
422
+ // Code Frame Formatting
423
+ // =============================================================================
424
+
425
+ /** Marker displayed in the gutter of a code frame. */
426
+ export type CodeFrameMarker = "" | " " | "*" | "+" | "-" | ">";
427
+
428
+ /** Format a code-frame row with a padded line-number gutter. */
429
+ export function formatCodeFrameLine(
430
+ marker: CodeFrameMarker,
431
+ lineNumber: string | number,
432
+ content: string,
433
+ lineNumberWidth: number,
434
+ ): string {
435
+ const markerText = marker.trim();
436
+ const lineNumberText = String(lineNumber).trim();
437
+ const gutterText = markerText && lineNumberText ? `${markerText}${lineNumberText}` : lineNumberText || markerText;
438
+ return `${gutterText.padStart(lineNumberWidth + 1, " ")}│${content}`;
439
+ }
440
+
441
+ // =============================================================================
442
+ // Tool UI Helpers
443
+ // =============================================================================
444
+
445
+ /** Status states supported by shared tool renderer icons. */
446
+ export type ToolUIStatus = "success" | "done" | "error" | "warning" | "info" | "pending" | "running" | "aborted";
447
+ /** Semantic foreground colors used by tool renderer labels. */
448
+ export type ToolUIColor = "success" | "error" | "warning" | "accent" | "muted";
449
+
450
+ /** Presentation options for a tool title. */
451
+ export interface ToolUITitleOptions {
452
+ bold?: boolean;
453
+ }
454
+
455
+ /** Render a themed tool title with optional bold emphasis. */
456
+ export function formatTitle(label: string, theme: Theme, options?: ToolUITitleOptions): string {
457
+ const content = options?.bold === false ? label : theme.bold(label);
458
+ return theme.fg("toolTitle", content);
459
+ }
460
+
461
+ // =============================================================================
462
+ // Diagnostic Formatting
463
+ // =============================================================================
464
+
465
+ /** Parsed diagnostic location, severity, and optional source and code metadata. */
466
+ export interface ParsedDiagnostic {
467
+ filePath: string;
468
+ line: number;
469
+ col: number;
470
+ severity: "error" | "warning" | "info" | "hint";
471
+ source?: string;
472
+ message: string;
473
+ code?: string;
474
+ }
475
+
476
+ /** Expand tabs in diagnostic text for terminal display. */
477
+ export function sanitizeDiagnosticDisplayText(text: string): string {
478
+ return replaceTabs(text);
479
+ }
480
+
481
+ function getSeverityRank(severity: ParsedDiagnostic["severity"]): number {
482
+ switch (severity) {
483
+ case "error":
484
+ return 0;
485
+ case "warning":
486
+ return 1;
487
+ case "info":
488
+ return 2;
489
+ case "hint":
490
+ return 3;
491
+ }
492
+ }
493
+
494
+ /** Parse a diagnostic location and message, including optional source and code. */
495
+ export function parseDiagnosticMessage(msg: string): ParsedDiagnostic | null {
496
+ const match = msg.match(/^(.+?):(\d+):(\d+)\s+\[(\w+)\]\s+(?:\[([^\]]+)\]\s+)?(.+?)(?:\s+\(([^)]+)\))?$/);
497
+ if (!match) return null;
498
+ return {
499
+ filePath: sanitizeDiagnosticDisplayText(match[1]),
500
+ line: parseInt(match[2], 10),
501
+ col: parseInt(match[3], 10),
502
+ severity: match[4] as ParsedDiagnostic["severity"],
503
+ source: match[5] ? sanitizeDiagnosticDisplayText(match[5]) : undefined,
504
+ message: sanitizeDiagnosticDisplayText(match[6]),
505
+ code: match[7] ? sanitizeDiagnosticDisplayText(match[7]) : undefined,
506
+ };
507
+ }
508
+
509
+ /** Render diagnostic summaries and optionally expanded messages. */
510
+ export function formatDiagnostics(
511
+ diag: { errored: boolean; summary: string; messages: string[] },
512
+ expanded: boolean,
513
+ theme: Theme,
514
+ getLangIcon: (filePath: string) => string,
515
+ options?: { title?: string },
516
+ ): string {
517
+ if (diag.messages.length === 0) return "";
518
+
519
+ const byFile = new Map<string, ParsedDiagnostic[]>();
520
+ const unparsed: string[] = [];
521
+
522
+ for (const msg of diag.messages) {
523
+ const parsed = parseDiagnosticMessage(msg);
524
+ if (parsed) {
525
+ const existing = byFile.get(parsed.filePath) ?? [];
526
+ existing.push(parsed);
527
+ byFile.set(parsed.filePath, existing);
528
+ } else {
529
+ unparsed.push(sanitizeDiagnosticDisplayText(msg));
530
+ }
531
+ }
532
+
533
+ for (const diagnostics of byFile.values()) {
534
+ diagnostics.sort((a, b) => {
535
+ const severityCompare = getSeverityRank(a.severity) - getSeverityRank(b.severity);
536
+ if (severityCompare !== 0) return severityCompare;
537
+ if (a.line !== b.line) return a.line - b.line;
538
+ if (a.col !== b.col) return a.col - b.col;
539
+ return a.message.localeCompare(b.message);
540
+ });
541
+ }
542
+
543
+ const headerIcon = diag.errored
544
+ ? theme.styledSymbol("status.error", "error")
545
+ : theme.styledSymbol("status.warning", "warning");
546
+ const summary = sanitizeDiagnosticDisplayText(diag.summary);
547
+ const summaryTag = summary ? ` ${theme.fg("dim", `(${summary})`)}` : "";
548
+ let output = `\n\n${headerIcon} ${theme.fg("toolTitle", options?.title ?? "Diagnostics")}${summaryTag}`;
549
+
550
+ const maxDiags = expanded ? diag.messages.length : 5;
551
+ let diagsShown = 0;
552
+
553
+ const files = Array.from(byFile.entries());
554
+
555
+ // Count total diagnostics for "... X more" calculation
556
+ const totalParsedDiags = files.reduce((sum, [, diags]) => sum + diags.length, 0);
557
+ const totalDiags = totalParsedDiags + unparsed.length;
558
+
559
+ // Helper to check if this is the very last item in the tree
560
+ const isTreeEnd = (fileIdx: number, diagIdx: number | null, unparsedIdx: number | null): boolean => {
561
+ const willShowMore = totalDiags > diagsShown + 1;
562
+ if (willShowMore) return false;
563
+
564
+ if (unparsedIdx !== null) {
565
+ return unparsedIdx === unparsed.length - 1;
566
+ }
567
+ if (diagIdx !== null) {
568
+ const isLastDiagInFile = diagIdx === files[fileIdx][1].length - 1;
569
+ const isLastFile = fileIdx === files.length - 1;
570
+ return isLastDiagInFile && isLastFile && unparsed.length === 0;
571
+ }
572
+ // File node - never the tree end if it has diagnostics
573
+ return false;
574
+ };
575
+
576
+ for (let fi = 0; fi < files.length && diagsShown < maxDiags; fi++) {
577
+ const [filePath, diagnostics] = files[fi];
578
+ // File is "last" only if no more files AND no unparsed AND we'll show all diags AND no "... X more"
579
+ const remainingDiagsInFile = diagnostics.length;
580
+ const remainingDiagsAfter = files.slice(fi + 1).reduce((sum, [, d]) => sum + d.length, 0) + unparsed.length;
581
+ const willShowAllRemaining = diagsShown + remainingDiagsInFile + remainingDiagsAfter <= maxDiags;
582
+ const isLastFileNode = fi === files.length - 1 && unparsed.length === 0 && willShowAllRemaining;
583
+ const fileBranch = isLastFileNode ? theme.tree.last : theme.tree.branch;
584
+
585
+ const fileIcon = theme.fg("muted", getLangIcon(filePath));
586
+ output += `\n ${theme.fg("dim", fileBranch)} ${fileIcon} ${theme.fg("accent", filePath)}`;
587
+
588
+ for (let di = 0; di < diagnostics.length && diagsShown < maxDiags; di++) {
589
+ const d = diagnostics[di];
590
+ const isLastDiagInFile = di === diagnostics.length - 1;
591
+ // This is the last visible diag in file if it's actually last OR we're about to hit the limit
592
+ const atDisplayLimit = diagsShown + 1 >= maxDiags;
593
+ const isLastVisibleInFile = isLastDiagInFile || atDisplayLimit;
594
+ // Check if this is the last visible item in the entire tree
595
+ const isVeryLast = isTreeEnd(fi, di, null);
596
+ const diagBranch = isLastFileNode
597
+ ? isLastVisibleInFile || isVeryLast
598
+ ? ` ${theme.tree.last}`
599
+ : ` ${theme.tree.branch}`
600
+ : isLastVisibleInFile || isVeryLast
601
+ ? `${theme.tree.vertical} ${theme.tree.last}`
602
+ : `${theme.tree.vertical} ${theme.tree.branch}`;
603
+
604
+ const sevIcon =
605
+ d.severity === "error"
606
+ ? theme.styledSymbol("status.error", "error")
607
+ : d.severity === "warning"
608
+ ? theme.styledSymbol("status.warning", "warning")
609
+ : theme.styledSymbol("status.info", "muted");
610
+ const location = theme.fg("dim", `:${d.line}:${d.col}`);
611
+ const codeTag = d.code ? theme.fg("dim", ` (${d.code})`) : "";
612
+ const msgColor = d.severity === "error" ? "error" : d.severity === "warning" ? "warning" : "toolOutput";
613
+
614
+ output += `\n ${theme.fg("dim", diagBranch)} ${sevIcon}${location} ${theme.fg(msgColor, d.message)}${codeTag}`;
615
+ diagsShown++;
616
+ }
617
+ }
618
+
619
+ for (let ui = 0; ui < unparsed.length && diagsShown < maxDiags; ui++) {
620
+ const msg = unparsed[ui];
621
+ const isVeryLast = isTreeEnd(-1, null, ui);
622
+ const branch = isVeryLast ? theme.tree.last : theme.tree.branch;
623
+ const color = msg.includes("[error]") ? "error" : msg.includes("[warning]") ? "warning" : "dim";
624
+ output += `\n ${theme.fg("dim", branch)} ${theme.fg(color, msg)}`;
625
+ diagsShown++;
626
+ }
627
+
628
+ if (totalDiags > diagsShown) {
629
+ const remaining = totalDiags - diagsShown;
630
+ output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
631
+ "muted",
632
+ `… ${remaining} more`,
633
+ )} ${formatExpandHint(theme)}`;
634
+ }
635
+
636
+ return output;
637
+ }
638
+
639
+ // =============================================================================
640
+ // Diff Utilities
641
+ // =============================================================================
642
+
643
+ /** Added and removed line counts and hunk metadata for a diff. */
644
+ export interface DiffStats {
645
+ added: number;
646
+ removed: number;
647
+ hunks: number;
648
+ lines: number;
649
+ }
650
+
651
+ /** Count added lines, removed lines, and hunks in a unified diff. */
652
+ export function getDiffStats(diffText: string): DiffStats {
653
+ const lines = diffText ? diffText.split("\n") : [];
654
+ let added = 0;
655
+ let removed = 0;
656
+ let hunks = 0;
657
+ let inHunk = false;
658
+
659
+ for (const line of lines) {
660
+ const isAdded = line.startsWith("+");
661
+ const isRemoved = line.startsWith("-");
662
+ const isChange = isAdded || isRemoved;
663
+
664
+ if (isAdded) added++;
665
+ if (isRemoved) removed++;
666
+
667
+ if (isChange && !inHunk) {
668
+ hunks++;
669
+ inHunk = true;
670
+ } else if (!isChange) {
671
+ inHunk = false;
672
+ }
673
+ }
674
+
675
+ return { added, removed, hunks, lines: lines.length };
676
+ }
677
+
678
+ /** Render a compact themed summary of diff counts. */
679
+ export function formatDiffStats(added: number, removed: number, hunks: number, theme: Theme): string {
680
+ const parts: string[] = [];
681
+ if (added > 0) parts.push(theme.fg("toolDiffAdded", `+${added}`));
682
+ if (removed > 0) parts.push(theme.fg("toolDiffRemoved", `-${removed}`));
683
+ if (hunks > 0) parts.push(theme.fg("dim", `${hunks} hunk${hunks !== 1 ? "s" : ""}`));
684
+ return parts.join(theme.fg("dim", " / "));
685
+ }
686
+
687
+ interface DiffSegment {
688
+ lines: string[];
689
+ isChange: boolean;
690
+ isEllipsis: boolean;
691
+ }
692
+
693
+ function parseDiffSegments(lines: string[]): DiffSegment[] {
694
+ const segments: DiffSegment[] = [];
695
+ let current: DiffSegment | null = null;
696
+
697
+ for (const line of lines) {
698
+ const isChange = line.startsWith("+") || line.startsWith("-");
699
+ const isEllipsis = line.trimStart().startsWith("...") || line.trim().length === 0;
700
+
701
+ if (isEllipsis) {
702
+ if (current) segments.push(current);
703
+ segments.push({ lines: [line], isChange: false, isEllipsis: true });
704
+ current = null;
705
+ } else if (!current || current.isChange !== isChange) {
706
+ if (current) segments.push(current);
707
+ current = { lines: [line], isChange, isEllipsis: false };
708
+ } else {
709
+ current.lines.push(line);
710
+ }
711
+ }
712
+
713
+ if (current) segments.push(current);
714
+ return segments;
715
+ }
716
+
717
+ /** Truncate a unified diff at hunk boundaries within the supplied preview limits. */
718
+ export function truncateDiffByHunk(
719
+ diffText: string,
720
+ maxHunks: number,
721
+ maxLines: number,
722
+ options?: { fromTail?: boolean },
723
+ ): { text: string; hiddenHunks: number; hiddenLines: number } {
724
+ if (options?.fromTail) {
725
+ // Streaming previews want to track the tail of the diff as new hunks
726
+ // arrive. Reversing the line buffer reuses the head-mode logic without
727
+ // duplicating the segment-budget bookkeeping: hunk runs survive
728
+ // reversal (a continuous `+`/`-` block stays contiguous) and so do the
729
+ // per-line `+`/`-` markers, so getDiffStats yields identical counts.
730
+ const reversed = (diffText ?? "").split("\n").reverse().join("\n");
731
+ const result = truncateDiffByHunk(reversed, maxHunks, maxLines);
732
+ return {
733
+ text: result.text.split("\n").reverse().join("\n"),
734
+ hiddenHunks: result.hiddenHunks,
735
+ hiddenLines: result.hiddenLines,
736
+ };
737
+ }
738
+ const lines = diffText ? diffText.split("\n") : [];
739
+ const totalStats = getDiffStats(diffText);
740
+
741
+ if (lines.length <= maxLines && totalStats.hunks <= maxHunks) {
742
+ return { text: diffText, hiddenHunks: 0, hiddenLines: 0 };
743
+ }
744
+
745
+ const segments = parseDiffSegments(lines);
746
+
747
+ const changeSegments = segments.filter(s => s.isChange);
748
+ const changeLineCount = changeSegments.reduce((sum, s) => sum + s.lines.length, 0);
749
+
750
+ if (changeLineCount > maxLines) {
751
+ const kept: string[] = [];
752
+ let keptHunks = 0;
753
+
754
+ for (const seg of segments) {
755
+ if (kept.length >= maxLines) break;
756
+ if (seg.isChange) {
757
+ if (keptHunks >= maxHunks) break;
758
+ keptHunks++;
759
+ }
760
+ const take = Math.min(seg.lines.length, maxLines - kept.length);
761
+ for (let i = 0; i < take; i++) {
762
+ kept.push(seg.lines[i]!);
763
+ }
764
+ }
765
+
766
+ return {
767
+ text: kept.join("\n"),
768
+ hiddenHunks: Math.max(0, totalStats.hunks - keptHunks),
769
+ hiddenLines: Math.max(0, lines.length - kept.length),
770
+ };
771
+ }
772
+
773
+ const contextBudget = maxLines - changeLineCount;
774
+ const contextSegments = segments.filter(s => !s.isChange);
775
+ const totalContextLines = contextSegments.reduce((sum, s) => sum + s.lines.length, 0);
776
+
777
+ const kept: string[] = [];
778
+ let keptHunks = 0;
779
+ let keptSourceLines = 0;
780
+
781
+ if (totalContextLines <= contextBudget) {
782
+ for (const seg of segments) {
783
+ if (seg.isChange) {
784
+ if (keptHunks >= maxHunks) break;
785
+ keptHunks++;
786
+ }
787
+ kept.push(...seg.lines);
788
+ keptSourceLines += seg.lines.length;
789
+ }
790
+ } else {
791
+ const contextRatio = totalContextLines > 0 ? contextBudget / totalContextLines : 0;
792
+ let remainingContextBudget = contextBudget;
793
+
794
+ for (let i = 0; i < segments.length; i++) {
795
+ const seg = segments[i];
796
+
797
+ if (seg.isChange) {
798
+ if (keptHunks >= maxHunks) break;
799
+ keptHunks++;
800
+ kept.push(...seg.lines);
801
+ keptSourceLines += seg.lines.length;
802
+ continue;
803
+ }
804
+ if (remainingContextBudget <= 0) continue;
805
+
806
+ const allowedLines = Math.min(
807
+ remainingContextBudget,
808
+ Math.max(1, Math.floor(seg.lines.length * contextRatio)),
809
+ );
810
+ const outputStart = kept.length;
811
+ let sourceLinesAdded = 0;
812
+
813
+ if (seg.isEllipsis || seg.lines.length <= allowedLines) {
814
+ for (let j = 0; j < allowedLines; j++) {
815
+ kept.push(seg.lines[j]!);
816
+ }
817
+ sourceLinesAdded = allowedLines;
818
+ } else {
819
+ const isBeforeChange = segments[i + 1]?.isChange;
820
+ const isAfterChange = segments[i - 1]?.isChange;
821
+
822
+ if (isBeforeChange && isAfterChange) {
823
+ if (allowedLines >= 3) {
824
+ const sourceBudget = allowedLines - 1;
825
+ const firstCount = Math.ceil(sourceBudget / 2);
826
+ const lastCount = sourceBudget - firstCount;
827
+ kept.push(...seg.lines.slice(0, firstCount));
828
+ kept.push("");
829
+ if (lastCount > 0) kept.push(...seg.lines.slice(-lastCount));
830
+ sourceLinesAdded = sourceBudget;
831
+ } else {
832
+ const firstCount = Math.ceil(allowedLines / 2);
833
+ const lastCount = allowedLines - firstCount;
834
+ kept.push(...seg.lines.slice(0, firstCount));
835
+ if (lastCount > 0) kept.push(...seg.lines.slice(-lastCount));
836
+ sourceLinesAdded = allowedLines;
837
+ }
838
+ } else if (isBeforeChange) {
839
+ kept.push(...seg.lines.slice(-allowedLines));
840
+ sourceLinesAdded = allowedLines;
841
+ } else if (isAfterChange) {
842
+ kept.push(...seg.lines.slice(0, allowedLines));
843
+ sourceLinesAdded = allowedLines;
844
+ } else {
845
+ const take = Math.min(allowedLines, 2);
846
+ kept.push(...seg.lines.slice(0, take));
847
+ sourceLinesAdded = take;
848
+ }
849
+ }
850
+
851
+ keptSourceLines += sourceLinesAdded;
852
+ remainingContextBudget -= kept.length - outputStart;
853
+ }
854
+ }
855
+
856
+ return {
857
+ text: kept.join("\n"),
858
+ hiddenHunks: Math.max(0, totalStats.hunks - keptHunks),
859
+ hiddenLines: Math.max(0, lines.length - keptSourceLines),
860
+ };
861
+ }
862
+
863
+ // =============================================================================
864
+ // Path Utilities
865
+ // =============================================================================
866
+
867
+ let cachedHomeDir: string | undefined;
868
+ let cachedHomedir: typeof os.homedir | undefined;
869
+
870
+ function defaultHomeDir(): string {
871
+ const homedir = os.homedir;
872
+ if (cachedHomeDir === undefined || cachedHomedir !== homedir) {
873
+ cachedHomedir = homedir;
874
+ cachedHomeDir = homedir();
875
+ }
876
+ return cachedHomeDir;
877
+ }
878
+
879
+ const homePatternCache = new Map<string, RegExp>();
880
+ function homePatternFor(homeDir: string, windowsStyle: boolean): RegExp {
881
+ const key = `${windowsStyle ? 1 : 0} ${homeDir}`;
882
+ let pattern = homePatternCache.get(key);
883
+ if (pattern === undefined) {
884
+ const escapedHome = windowsStyle
885
+ ? homeDir
886
+ .replaceAll("/", "\\")
887
+ .split("\\")
888
+ .map(part => RegExp.escape(part))
889
+ .join("[\\\\/]")
890
+ : RegExp.escape(homeDir);
891
+ pattern = new RegExp(
892
+ `[a-zA-Z][a-zA-Z0-9+.-]*://[^\\s"'<>]+|(^|[\\s"'\\x60([{=,:])(${escapedHome})(?=$|[\\\\/\\s"'\\x60)\\]},;:])`,
893
+ windowsStyle ? "gi" : "g",
894
+ );
895
+ if (homePatternCache.size >= 16) homePatternCache.clear();
896
+ homePatternCache.set(key, pattern);
897
+ }
898
+ return pattern;
899
+ }
900
+
901
+ /** Replace a leading home directory with a portable tilde prefix. */
902
+ export function shortenPath(filePath: unknown, homeDir?: string): string {
903
+ if (typeof filePath !== "string") {
904
+ return "";
905
+ }
906
+ const home = homeDir ?? defaultHomeDir();
907
+ const windowsStyle = /^[A-Za-z]:[\\/]/.test(home) || home.startsWith("\\\\");
908
+ const hasHomePrefix = windowsStyle
909
+ ? filePath.toLowerCase().startsWith(home.toLowerCase())
910
+ : filePath.startsWith(home);
911
+ if (home && hasHomePrefix) {
912
+ const suffix = filePath.slice(home.length);
913
+ if (suffix === "" || suffix.startsWith(path.posix.sep) || suffix.startsWith(path.win32.sep)) {
914
+ return `~${suffix.replaceAll(path.win32.sep, path.posix.sep)}`;
915
+ }
916
+ }
917
+ return filePath;
918
+ }
919
+
920
+ /** Shorten embedded home paths; normalize Windows separators unless the caller preserves native error text. */
921
+ export function shortenEmbeddedPaths(text: string, homeDir?: string, preserveSeparators = false): string {
922
+ const resolvedHome = homeDir ?? defaultHomeDir();
923
+ if (!resolvedHome || resolvedHome.length <= 1) return text;
924
+ const windowsStyle = /^[A-Za-z]:[\\/]/.test(resolvedHome) || resolvedHome.startsWith("\\\\");
925
+ const homePattern = homePatternFor(resolvedHome, windowsStyle);
926
+ const textWithShortenedHome = text.replace(
927
+ homePattern,
928
+ (match, boundary: string | undefined, candidate: string | undefined) =>
929
+ candidate === undefined ? match : `${boundary}~`,
930
+ );
931
+ if (preserveSeparators) return textWithShortenedHome;
932
+ return textWithShortenedHome
933
+ .split(" ")
934
+ .map(segment => {
935
+ const leading = segment.match(/^[("'`[]*/)?.[0] ?? "";
936
+ const trailing = segment.match(/[)"'`,.;:\]]*$/)?.[0] ?? "";
937
+ const end = segment.length - trailing.length;
938
+ if (leading.length >= end) return segment;
939
+ const shortened = shortenPath(segment.slice(leading.length, end), resolvedHome);
940
+ const normalized = shortened.startsWith("~")
941
+ ? shortened.replaceAll(path.win32.sep, path.posix.sep)
942
+ : shortened;
943
+ return `${leading}${normalized}${trailing}`;
944
+ })
945
+ .join(" ");
946
+ }
947
+
948
+ /** Sanitize warning text before showing it in TUI, including embedded home paths. */
949
+ export function sanitizeDisplayWarning(text: string): string {
950
+ return shortenEmbeddedPaths(
951
+ replaceTabs(sanitizeText(text))
952
+ .replace(/[\r\n]+/g, " ")
953
+ .trim(),
954
+ );
955
+ }
956
+
957
+ /** Sanitize and bound warning text before showing it in TUI. */
958
+ export function sanitizeDisplayWarnings(warnings: readonly string[]): string[] {
959
+ const visible = warnings
960
+ .slice(0, PREVIEW_LIMITS.COLLAPSED_ITEMS)
961
+ .map(warning => truncateToWidth(sanitizeDisplayWarning(warning), TRUNCATE_LENGTHS.LONG));
962
+ const hidden = warnings.length - visible.length;
963
+ if (hidden > 0) visible.push(`… ${hidden} more ${pluralize("warning", hidden)}`);
964
+ return visible;
965
+ }
966
+
967
+ /** Format a tool working directory when it differs from the project directory. */
968
+ export function formatToolWorkingDirectory(workdir: string | undefined, projectDir: string): string | undefined {
969
+ if (!workdir) return undefined;
970
+ const resolvedProjectDir = path.resolve(projectDir);
971
+ const resolvedWorkdir = path.resolve(projectDir, workdir);
972
+ if (resolvedWorkdir === resolvedProjectDir) {
973
+ return undefined;
974
+ }
975
+ const relativePath = path.relative(resolvedProjectDir, resolvedWorkdir);
976
+ const isWithinProject =
977
+ relativePath.length > 0 && !relativePath.startsWith("..") && !relativePath.startsWith(`..${path.sep}`);
978
+ const displayWorkdir = isWithinProject ? relativePath : shortenPath(resolvedWorkdir);
979
+ return replaceTabs(displayWorkdir);
980
+ }
981
+
982
+ /** Wrap text in the theme-specific bracket pair. */
983
+ export function wrapBrackets(text: string, theme: Theme): string {
984
+ return `${theme.format.bracketLeft}${text}${theme.format.bracketRight}`;
985
+ }
986
+
987
+ /** Maximum number of parse errors displayed in expanded details. */
988
+ export const PARSE_ERRORS_LIMIT = 20;
989
+
990
+ /** Deduplicate parse errors while preserving their original order. */
991
+ export function dedupeParseErrors(errors: string[] | undefined): string[] {
992
+ if (!errors || errors.length === 0) return [];
993
+ const seen = new Set<string>();
994
+ const deduped: string[] = [];
995
+ for (const error of errors) {
996
+ if (seen.has(error)) continue;
997
+ seen.add(error);
998
+ deduped.push(error);
999
+ }
1000
+ return deduped;
1001
+ }
1002
+
1003
+ /** Format deduplicated parse errors with a bounded detail list. */
1004
+ export function formatParseErrors(errors: string[], total?: number): string[] {
1005
+ const deduped = dedupeParseErrors(errors);
1006
+ if (deduped.length === 0) return [];
1007
+ const fullCount = total ?? deduped.length;
1008
+ const capped = deduped.slice(0, PARSE_ERRORS_LIMIT);
1009
+ const header = fullCount > capped.length ? `Parse issues (${capped.length} / ${fullCount}):` : "Parse issues:";
1010
+ return [header, ...capped.map(err => `- ${err}`)];
1011
+ }
1012
+
1013
+ /**
1014
+ * Cap an upstream parse-error list to {@link PARSE_ERRORS_LIMIT} unique entries,
1015
+ * preserving the original deduplicated total. Use this at the source so tool
1016
+ * details never carry thousands of per-file parse errors into traces or
1017
+ * renderers.
1018
+ */
1019
+ export function capParseErrors(
1020
+ errors: string[] | undefined,
1021
+ limit: number = PARSE_ERRORS_LIMIT,
1022
+ ): { errors: string[]; total: number } {
1023
+ const deduped = dedupeParseErrors(errors);
1024
+ return { errors: deduped.slice(0, limit), total: deduped.length };
1025
+ }
1026
+
1027
+ // =============================================================================
1028
+ // Renderer helpers shared by search / find / ast tools
1029
+ // =============================================================================
1030
+
1031
+ /**
1032
+ * Standard width+expand keyed render cache used by every search-style tool
1033
+ * renderer. `compute` re-runs only when the cache key changes; the returned
1034
+ * Component is the canonical `{ render, invalidate }` pair.
1035
+ */
1036
+ export function createCachedComponent(
1037
+ getExpanded: () => boolean,
1038
+ compute: (width: number, expanded: boolean) => string[],
1039
+ options: { paddingX?: number } = {},
1040
+ ): Component {
1041
+ let cached: { key: bigint; lines: string[] } | undefined;
1042
+ return {
1043
+ render(width: number): readonly string[] {
1044
+ const expanded = getExpanded();
1045
+ const key = new Hasher().bool(expanded).u32(width).digest();
1046
+ if (cached?.key === key) return cached.lines;
1047
+ const paddingX = Math.max(0, options.paddingX ?? 0);
1048
+ const innerWidth = Math.max(1, width - paddingX * 2);
1049
+ const lines = compute(innerWidth, expanded);
1050
+ const pad = paddingX === 0 ? "" : " ".repeat(paddingX);
1051
+ const paddedLines = paddingX === 0 ? lines : lines.map(line => `${pad}${line}${pad}`);
1052
+ cached = { key, lines: paddedLines };
1053
+ return paddedLines;
1054
+ },
1055
+ invalidate() {
1056
+ cached = undefined;
1057
+ },
1058
+ };
1059
+ }
1060
+
1061
+ /**
1062
+ * Single-slot memo for an expensive rendered string (syntax highlighting, diff
1063
+ * coloring) keyed by the exact inputs that shape the bytes: theme instance,
1064
+ * expanded state, a caller-chosen salt (path/language), and the source content.
1065
+ * Field-wise comparison instead of a concatenated key string: a cache hit costs
1066
+ * one string value-compare (engines short-circuit on length) and a miss never
1067
+ * allocates a key. Comparing the {@link Theme} by reference is sound because
1068
+ * theme switches replace the instance wholesale (`setTheme`/`previewTheme`/
1069
+ * `setSymbolPreset` in pi-tui theme/theme.ts) — themes are never mutated in
1070
+ * place.
1071
+ */
1072
+ export interface RenderedStringCache {
1073
+ theme: Theme | null;
1074
+ expanded: boolean;
1075
+ salt: string;
1076
+ content: string;
1077
+ value: string;
1078
+ }
1079
+
1080
+ /** Create an empty memo for rendered tool strings. */
1081
+ export function createRenderedStringCache(): RenderedStringCache {
1082
+ return { theme: null, expanded: false, salt: "", content: "", value: "" };
1083
+ }
1084
+
1085
+ /** Drop the memo so the next lookup re-renders (e.g. the render function identity changed). */
1086
+ export function invalidateRenderedStringCache(cache: RenderedStringCache): void {
1087
+ cache.theme = null;
1088
+ }
1089
+
1090
+ /** Reuse a rendered string while its theme, expansion, content, and salt match. */
1091
+ export function cachedRenderedString(
1092
+ cache: RenderedStringCache | undefined,
1093
+ theme: Theme,
1094
+ expanded: boolean,
1095
+ salt: string,
1096
+ content: string,
1097
+ render: () => string,
1098
+ ): string {
1099
+ if (
1100
+ cache !== undefined &&
1101
+ cache.theme === theme &&
1102
+ cache.expanded === expanded &&
1103
+ cache.salt === salt &&
1104
+ cache.content === content
1105
+ ) {
1106
+ return cache.value;
1107
+ }
1108
+ const value = render();
1109
+ if (cache !== undefined) {
1110
+ cache.theme = theme;
1111
+ cache.expanded = expanded;
1112
+ cache.salt = salt;
1113
+ cache.content = content;
1114
+ cache.value = value;
1115
+ }
1116
+ return value;
1117
+ }
1118
+
1119
+ /**
1120
+ * Append the indented bullet list of parse errors (capped at
1121
+ * {@link PARSE_ERRORS_LIMIT}) to `lines`, with an overflow summary line if the
1122
+ * total exceeds the cap. No-op when `parseErrors` is empty.
1123
+ */
1124
+ export function appendParseErrorsBulletList(
1125
+ lines: string[],
1126
+ parseErrors: readonly string[] | undefined,
1127
+ theme: Theme,
1128
+ total?: number,
1129
+ ): void {
1130
+ if (!parseErrors || parseErrors.length === 0) return;
1131
+ const fullCount = total ?? parseErrors.length;
1132
+ const capped = parseErrors.slice(0, PARSE_ERRORS_LIMIT);
1133
+ for (const err of capped) {
1134
+ lines.push(theme.fg("warning", ` - ${err}`));
1135
+ }
1136
+ if (fullCount > capped.length) {
1137
+ lines.push(theme.fg("dim", ` … ${fullCount - capped.length} more`));
1138
+ }
1139
+ }
1140
+
1141
+ /**
1142
+ * Human-readable summary string for the parse-issues count, capped by
1143
+ * {@link PARSE_ERRORS_LIMIT}.
1144
+ */
1145
+ export function formatParseErrorsCountLabel(parseErrors: readonly string[], total?: number): string {
1146
+ const fullCount = total ?? parseErrors.length;
1147
+ return fullCount > PARSE_ERRORS_LIMIT
1148
+ ? `${PARSE_ERRORS_LIMIT} / ${fullCount} parse issues`
1149
+ : `${fullCount} parse issue${fullCount !== 1 ? "s" : ""}`;
1150
+ }
1151
+
1152
+ /**
1153
+ * Parse a JSON-encoded array of path strings (e.g. `'["a.ts","b.ts"]'`).
1154
+ * Returns `null` when the input is not a bracketed JSON string array, so the
1155
+ * caller can fall back to treating the input as a single literal path.
1156
+ */
1157
+ function parseStringEncodedPathArray(input: string): string[] | null {
1158
+ const trimmed = input.trim();
1159
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null;
1160
+
1161
+ let parsed: unknown;
1162
+ try {
1163
+ parsed = JSON.parse(trimmed);
1164
+ } catch {
1165
+ return null;
1166
+ }
1167
+
1168
+ if (!Array.isArray(parsed) || parsed.some(entry => typeof entry !== "string")) {
1169
+ return null;
1170
+ }
1171
+ return parsed;
1172
+ }
1173
+
1174
+ /**
1175
+ * Normalize a path argument that may arrive as a single string, a JSON-encoded
1176
+ * string array (`'["a.ts"]'`), or an actual array into a flat `string[]`.
1177
+ * Delimited single strings (`"a.ts b.ts"`) remain literal.
1178
+ */
1179
+ export function toPathList(input: string | string[] | undefined): string[] {
1180
+ if (typeof input === "string") return parseStringEncodedPathArray(input) ?? [input];
1181
+ return input ?? [];
1182
+ }