@oh-my-pi/pi-tui 18.2.4 → 18.2.6

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,1258 @@
1
+ /**
2
+ * Edit tool renderer.
3
+ */
4
+
5
+ import { type EditInspection, editInspect } from "@oh-my-pi/pi-natives";
6
+ import type { Component } from "../tui";
7
+ import { sliceWithWidth, visibleWidth, wrapTextWithAnsi } from "../utils";
8
+ import { sanitizeText } from "@oh-my-pi/pi-utils";
9
+ import type { RenderResultOptions, ToolRenderer } from "./renderer";
10
+ import type { FileDiagnosticsResult } from "./lsp";
11
+ import { renderDiff as renderDiffColored } from "../chrome/diff";
12
+ import { getLanguageFromPath } from "../lang-from-path";
13
+ import type { Theme } from "../theme/theme";
14
+ import type { OutputMeta } from "./output-meta";
15
+ import {
16
+ cachedRenderedString,
17
+ cappedHeadLines,
18
+ createRenderedStringCache,
19
+ formatDiagnostics,
20
+ formatExpandHint,
21
+ formatStatusIcon,
22
+ getDiffStats,
23
+ invalidateRenderedStringCache,
24
+ PREVIEW_LIMITS,
25
+ previewWindowRows,
26
+ type RenderedStringCache,
27
+ replaceTabs,
28
+ shortenPath,
29
+ truncateDiffByHunk,
30
+ } from "../render/render-utils";
31
+ import type { ToolActivityContext, ToolActivitySummary } from "./renderer";
32
+ import { fileHyperlink, Hasher, type RenderCache, renderStatusLine, truncateToWidth, WidthAwareText } from "../render";
33
+ import { framedToolCard } from "../render/tool-card";
34
+ import { HL_FILE_PREFIX, HL_FILE_SUFFIX, HL_MOVE_KEYWORD, HL_REM_KEYWORD } from "./hashline-format";
35
+
36
+ /** Edit payload syntax selected by the caller. */
37
+ export type EditMode = "replace" | "patch" | "hashline" | "apply_patch" | "sloppy";
38
+
39
+ // ═══════════════════════════════════════════════════════════════════════════
40
+ // Tool Details Types
41
+ // ═══════════════════════════════════════════════════════════════════════════
42
+
43
+ /** Filesystem operation represented by an edit. */
44
+ export type Operation = "create" | "delete" | "update";
45
+
46
+ /** Computed streaming diff or preview error for one file. */
47
+ export interface PerFileDiffPreview {
48
+ path: string;
49
+ diff?: string;
50
+ firstChangedLine?: number;
51
+ error?: string;
52
+ }
53
+
54
+ type EditDiffPreview =
55
+ | { diff: string; firstChangedLine?: number }
56
+ | { error: string; diff?: undefined; firstChangedLine?: undefined };
57
+
58
+ /** Result metadata for one edited file. */
59
+ export interface EditToolPerFileResult {
60
+ path: string;
61
+ diff: string;
62
+ firstChangedLine?: number;
63
+ diagnostics?: FileDiagnosticsResult;
64
+ op?: Operation;
65
+ move?: string;
66
+ isError?: boolean;
67
+ errorText?: string;
68
+ /** TUI-friendly error text. When present, rendered to the user instead of `errorText`.
69
+ * Set when the underlying error carries a `displayMessage` (e.g. {@link HashlineMismatchError}). */
70
+ displayErrorText?: string;
71
+ meta?: OutputMeta;
72
+ /** Source-of-truth content before the edit; `undefined` for create operations. */
73
+ oldText?: string;
74
+ /** Source-of-truth content after the edit; `undefined` for delete operations. */
75
+ newText?: string;
76
+ /** True when {@link pruneOversizedEditSnapshots} dropped `oldText`/`newText` from this entry. Aggregators check this to suppress misleading combined snapshots when at least one entry of a multi-entry single-path edit was pruned. */
77
+ snapshotsPruned?: boolean;
78
+ /** Pre-move source path; set only when the edit moved/renamed the file. The header renders `sourcePath → path`. */
79
+ sourcePath?: string;
80
+ }
81
+
82
+ /** Diff, diagnostics, and snapshots returned by an edit. */
83
+ export interface EditToolDetails {
84
+ /** Unified diff of the changes made */
85
+ diff: string;
86
+ /** Line number of the first change in the new file (for editor navigation) */
87
+ firstChangedLine?: number;
88
+ /** Diagnostic result (if available) */
89
+ diagnostics?: FileDiagnosticsResult;
90
+ /** Operation type (patch mode only) */
91
+ op?: Operation;
92
+ /** New path after move/rename (patch mode only) */
93
+ move?: string;
94
+ /** Structured output metadata */
95
+ meta?: OutputMeta;
96
+ /** Per-file results (multi-file edits) */
97
+ perFileResults?: EditToolPerFileResult[];
98
+ /** Absolute file path for single-file edit results. Required by ACP diff metadata consumers. */
99
+ path?: string;
100
+ /** Source-of-truth content before the edit; `undefined` for create operations. */
101
+ oldText?: string;
102
+ /** Source-of-truth content after the edit; `undefined` for delete operations. */
103
+ newText?: string;
104
+ /** True when {@link pruneOversizedEditSnapshots} dropped `oldText`/`newText` from this entry. Aggregators check this to suppress misleading combined snapshots when at least one entry of a multi-entry single-path edit was pruned. */
105
+ snapshotsPruned?: boolean;
106
+ /** Pre-move source path; set only when the edit moved/renamed the file. The header renders `sourcePath → path`. */
107
+ sourcePath?: string;
108
+ }
109
+
110
+ // ═══════════════════════════════════════════════════════════════════════════
111
+ // TUI Renderer
112
+ // ═══════════════════════════════════════════════════════════════════════════
113
+
114
+ /** Memoized native inspection of the streamed payload, tagged onto the args it came from. */
115
+ const kInspectedInput = Symbol("edit.inspectedInput");
116
+
117
+ type InspectedInput = { mode: EditMode; input: string } & (
118
+ | { entries: InspectedInputEntry[]; error?: undefined }
119
+ | { entries?: undefined; error: unknown }
120
+ );
121
+
122
+ interface EditRenderArgs {
123
+ path?: unknown;
124
+ file_path?: unknown;
125
+ oldText?: string;
126
+ newText?: string;
127
+ patch?: string;
128
+ input?: string;
129
+ _input?: string;
130
+ replace_all?: boolean;
131
+ // Patch mode fields
132
+ op?: Operation;
133
+ rename?: unknown;
134
+ diff?: string;
135
+ /**
136
+ * Computed preview diff (used when tool args don't include a diff, e.g. hashline mode).
137
+ */
138
+ previewDiff?: string;
139
+ __partialJson?: string;
140
+ // Hashline mode fields
141
+ edits?: EditRenderEntry[];
142
+ [kInspectedInput]?: InspectedInput;
143
+ }
144
+
145
+ type EditRenderEntry = {
146
+ path?: unknown;
147
+ rename?: unknown;
148
+ move?: unknown;
149
+ op?: Operation;
150
+ };
151
+
152
+ interface HashlineInputEntry {
153
+ path: string;
154
+ op?: Operation;
155
+ rename?: string;
156
+ /** A PUT/CUT line edit precedes the file op — keeps a move framed. */
157
+ hasLineEdits?: boolean;
158
+ }
159
+
160
+ interface HashlineInputRenderSummary {
161
+ entries: HashlineInputEntry[];
162
+ }
163
+
164
+ interface InspectedInputEntry {
165
+ path: string;
166
+ op?: Operation;
167
+ rename?: string;
168
+ }
169
+
170
+ interface ApplyPatchRenderSummary {
171
+ entries: InspectedInputEntry[];
172
+ error?: string;
173
+ }
174
+
175
+ /** Extended context for edit tool rendering */
176
+ export interface EditRenderContext {
177
+ /** Edit mode resolved by the caller; lets the renderer dispatch without shape-sniffing */
178
+ editMode?: EditMode;
179
+ /** Pre-computed diff preview (computed before tool executes) */
180
+ editDiffPreview?: EditDiffPreview;
181
+ /** Multi-file streaming diff preview (edits spanning several files) */
182
+ perFileDiffPreview?: PerFileDiffPreview[];
183
+ /** Raw in-flight edit text shown while a computed diff preview is unavailable */
184
+ editStreamingFallback?: string;
185
+ /** Function to render diff text with syntax highlighting */
186
+ renderDiff?: (diffText: string, options?: { filePath?: string }) => string;
187
+ }
188
+
189
+ /**
190
+ * Lazily grown per-file preview cache slots: the file count of a streaming
191
+ * multi-file patch is discovered mid-stream, so a fixed-size array would
192
+ * silently bypass caching for late files.
193
+ */
194
+ function previewCacheAt(caches: RenderedStringCache[] | undefined, index: number): RenderedStringCache | undefined {
195
+ if (!caches) return undefined;
196
+ let cache = caches[index];
197
+ if (cache === undefined) {
198
+ cache = createRenderedStringCache();
199
+ caches[index] = cache;
200
+ }
201
+ return cache;
202
+ }
203
+
204
+ const CALL_TEXT_PREVIEW_LINES = 6;
205
+ const CALL_TEXT_PREVIEW_WIDTH = 80;
206
+
207
+ /** Render replacement text until a computed diff preview is available. */
208
+ export function renderStreamingFallback(editMode: EditMode, args: unknown, theme: Theme): string {
209
+ if (editMode !== "replace" || !args || typeof args !== "object") return "";
210
+ const record = args as Record<string, unknown>;
211
+ const newString =
212
+ typeof record.new_string === "string"
213
+ ? record.new_string
214
+ : typeof record.newText === "string"
215
+ ? record.newText
216
+ : undefined;
217
+ return newString ? renderPlainTextPreview(newString, theme) : "";
218
+ }
219
+
220
+ /** Extract file path from an edit entry. */
221
+ function filePathFromEditEntry(p: unknown): string | undefined {
222
+ if (typeof p !== "string") {
223
+ return undefined;
224
+ }
225
+ return p;
226
+ }
227
+
228
+ function decodePartialJsonStringFragment(fragment: string): string {
229
+ // Trim a trailing partial escape so JSON.parse sees a well-formed string.
230
+ let text = fragment.replace(/\\u[0-9a-fA-F]{0,3}$/, "");
231
+ const trailingBackslashes = text.match(/\\+$/)?.[0].length ?? 0;
232
+ if (trailingBackslashes % 2 === 1) text = text.slice(0, -1);
233
+ try {
234
+ return JSON.parse(`"${text}"`) as string;
235
+ } catch {
236
+ // Streaming fragment isn't a valid JSON string yet; surface it raw rather
237
+ // than ad-hoc unescaping that mishandles surrogates and partial escapes.
238
+ return text;
239
+ }
240
+ }
241
+
242
+ function extractPartialJsonString(partialJson: string | undefined, key: string): string | undefined {
243
+ if (!partialJson) return undefined;
244
+ const pattern = new RegExp(`"${key}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)`, "u");
245
+ const match = pattern.exec(partialJson);
246
+ if (!match) return undefined;
247
+ return decodePartialJsonStringFragment(match[1]);
248
+ }
249
+
250
+ function getPartialJsonEditPath(args: EditRenderArgs): string | undefined {
251
+ return filePathFromEditEntry(extractPartialJsonString(args.__partialJson, "path"));
252
+ }
253
+
254
+ /** Count distinct file paths in an edits array. */
255
+ function countEditFiles(edits: EditRenderEntry[]): number {
256
+ return new Set(edits.map(edit => filePathFromEditEntry(edit.path)).filter(Boolean)).size;
257
+ }
258
+
259
+ function getOperationTitle(op: Operation | undefined): string {
260
+ return op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
261
+ }
262
+
263
+ interface EditPathDisplayOptions {
264
+ rename?: string;
265
+ firstChangedLine?: number;
266
+ linkPath?: string;
267
+ renameLinkPath?: string;
268
+ maxPathWidth?: number;
269
+ }
270
+
271
+ function truncateEditTitlePath(displayPath: string, maxWidth: number | undefined): string {
272
+ if (maxWidth === undefined) return displayPath;
273
+ const width = visibleWidth(displayPath);
274
+ const safeMaxWidth = Math.max(0, Math.floor(maxWidth));
275
+ if (width <= safeMaxWidth) return displayPath;
276
+
277
+ const contentWidth = safeMaxWidth - 1;
278
+ if (contentWidth <= 0) return "…";
279
+
280
+ const headWidth = Math.floor(contentWidth / 2);
281
+ const tailWidth = contentWidth - headWidth;
282
+ const head = sliceWithWidth(displayPath, 0, headWidth, true).text;
283
+ const tail = sliceWithWidth(displayPath, Math.max(0, width - tailWidth), tailWidth, true).text;
284
+ return `${head}…${tail}`;
285
+ }
286
+
287
+ function formatEditTitlePath(pathValue: string, maxWidth?: number): string {
288
+ return truncateEditTitlePath(replaceTabs(shortenPath(pathValue)), maxWidth);
289
+ }
290
+
291
+ function formatEditPathDisplay(
292
+ rawPath: string,
293
+ uiTheme: Theme,
294
+ options?: EditPathDisplayOptions,
295
+ ): { text: string; pathWidth: number } {
296
+ // `rawPath`/`rename` are shown (cwd-relative) but the OSC 8 link targets the
297
+ // absolute path when known — a relative `rawPath` would otherwise yield a
298
+ // `file:///rel` URI that resolves against filesystem root instead of cwd.
299
+ const linkTarget = options?.linkPath || rawPath;
300
+ const lineLink = options?.firstChangedLine ? { line: options.firstChangedLine } : undefined;
301
+ const primaryDisplay = rawPath ? formatEditTitlePath(rawPath, options?.maxPathWidth) : "…";
302
+ let pathDisplay = rawPath
303
+ ? fileHyperlink(linkTarget, uiTheme.fg("accent", primaryDisplay), lineLink)
304
+ : uiTheme.fg("toolOutput", primaryDisplay);
305
+ let pathWidth = visibleWidth(primaryDisplay);
306
+
307
+ if (options?.rename) {
308
+ const renameTarget = options.renameLinkPath || options.rename;
309
+ const renameDisplay = formatEditTitlePath(options.rename, options.maxPathWidth);
310
+ pathDisplay += ` ${uiTheme.fg("dim", "→")} ${fileHyperlink(renameTarget, uiTheme.fg("accent", renameDisplay))}`;
311
+ pathWidth += visibleWidth(renameDisplay);
312
+ }
313
+
314
+ return { text: pathDisplay, pathWidth };
315
+ }
316
+
317
+ function formatEditDescription(
318
+ rawPath: string,
319
+ uiTheme: Theme,
320
+ options?: EditPathDisplayOptions,
321
+ ): { language: string; description: string; pathWidth: number } {
322
+ const language = getLanguageFromPath(rawPath) ?? "text";
323
+ const icon = uiTheme.fg("muted", uiTheme.getLangIcon(language));
324
+ const pathDisplay = formatEditPathDisplay(rawPath, uiTheme, options);
325
+ return {
326
+ language,
327
+ description: `${icon} ${pathDisplay.text}`,
328
+ pathWidth: pathDisplay.pathWidth,
329
+ };
330
+ }
331
+
332
+ function editHeaderLabelBudget(width: number, uiTheme: Theme): number {
333
+ const leftGlyphs = `${uiTheme.boxRound.topLeft}${uiTheme.boxRound.horizontal.repeat(3)}`;
334
+ return Math.max(0, width - visibleWidth(leftGlyphs) - visibleWidth(uiTheme.boxRound.topRight) - 2);
335
+ }
336
+
337
+ function renderEditHeader(
338
+ width: number,
339
+ uiTheme: Theme,
340
+ options: {
341
+ icon?: "pending" | "success" | "error";
342
+ iconOverride?: string;
343
+ op?: Operation;
344
+ rawPath: string;
345
+ rename?: string;
346
+ firstChangedLine?: number;
347
+ linkPath?: string;
348
+ statsSuffix?: string;
349
+ extraSuffix?: string;
350
+ title?: string;
351
+ },
352
+ ): string {
353
+ const title = options.title ?? getOperationTitle(options.op);
354
+ const descriptionOptions: EditPathDisplayOptions = {
355
+ rename: options.rename,
356
+ firstChangedLine: options.firstChangedLine,
357
+ linkPath: options.linkPath,
358
+ };
359
+ const formatted = formatEditDescription(options.rawPath, uiTheme, descriptionOptions);
360
+ const suffix = `${options.statsSuffix ?? ""}${options.extraSuffix ?? ""}`;
361
+ const buildHeader = (description: string): string =>
362
+ renderStatusLine(
363
+ {
364
+ icon: options.icon,
365
+ iconOverride: options.iconOverride,
366
+ title,
367
+ description,
368
+ },
369
+ uiTheme,
370
+ ) + suffix;
371
+
372
+ const header = buildHeader(formatted.description);
373
+ const overflow = visibleWidth(header) - editHeaderLabelBudget(width, uiTheme);
374
+ if (overflow <= 0 || formatted.pathWidth <= 1) return header;
375
+
376
+ const pathCount = Math.max(1, (options.rawPath ? 1 : 0) + (options.rename ? 1 : 0));
377
+ const fittedPathWidth = Math.max(1, Math.floor((formatted.pathWidth - overflow) / pathCount));
378
+ const fitted = formatEditDescription(options.rawPath, uiTheme, {
379
+ ...descriptionOptions,
380
+ maxPathWidth: fittedPathWidth,
381
+ });
382
+ return buildHeader(fitted.description);
383
+ }
384
+
385
+ /**
386
+ * Inline status row for delete / move-only edits — they carry no diff, so they
387
+ * render as a single line instead of an empty framed container. The completed
388
+ * result uses the eraser/move glyph; a still-streaming call uses the shared
389
+ * pending hourglass like every other tool.
390
+ */
391
+ function renderInlineEditRow(
392
+ uiTheme: Theme,
393
+ opts: { op?: Operation; rename?: string; rawPath: string; linkPath?: string; pending: boolean },
394
+ ): Component {
395
+ const isDelete = opts.op === "delete";
396
+ return new WidthAwareText(
397
+ width =>
398
+ renderEditHeader(width, uiTheme, {
399
+ icon: opts.pending ? "pending" : undefined,
400
+ iconOverride: opts.pending
401
+ ? undefined
402
+ : uiTheme.styledSymbol(isDelete ? "tool.delete" : "tool.move", "accent"),
403
+ op: opts.op,
404
+ title: isDelete ? "Delete" : "Move",
405
+ rawPath: opts.rawPath,
406
+ rename: opts.rename,
407
+ linkPath: opts.linkPath,
408
+ }),
409
+ 0,
410
+ 0,
411
+ );
412
+ }
413
+
414
+ /**
415
+ * Whether a streaming edit call carries any payload worth boxing (a diff
416
+ * preview, replacement text, or a non-empty edits array). Used to keep a
417
+ * move-with-edits framed while a payload-less move/delete folds to an inline
418
+ * row — gated on args, not the async preview, so it can't flash inline before
419
+ * the diff arrives.
420
+ */
421
+ function hasEditCallPayload(args: EditRenderArgs, renderContext: EditRenderContext | undefined): boolean {
422
+ const multi = renderContext?.perFileDiffPreview;
423
+ if (multi && multi.length > 1 && multi.some(p => p.diff || p.error)) return true;
424
+ if (args.previewDiff || args.diff || args.newText || args.patch) return true;
425
+ if (Array.isArray(args.edits) && args.edits.length > 0) return true;
426
+ if (renderContext?.editStreamingFallback) return true;
427
+ return false;
428
+ }
429
+
430
+ function renderPlainTextPreview(text: string, uiTheme: Theme, _filePath?: string): string {
431
+ const previewLines = cappedHeadLines(sanitizeText(text).split("\n"), CALL_TEXT_PREVIEW_LINES);
432
+ let preview = "\n\n";
433
+ for (const line of previewLines.lines) {
434
+ preview += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), CALL_TEXT_PREVIEW_WIDTH))}\n`;
435
+ }
436
+ if (previewLines.hidden > 0) {
437
+ preview += uiTheme.fg("dim", `… ${previewLines.hidden} more lines`);
438
+ }
439
+ return preview.trimEnd();
440
+ }
441
+
442
+ interface StreamingDiffTail {
443
+ content: string;
444
+ hidden: boolean;
445
+ }
446
+
447
+ /**
448
+ * Select the trailing physical lines that fit the live preview budget.
449
+ *
450
+ * Walk backward from the end instead of splitting the complete diff. This keeps
451
+ * allocation and scanning proportional to the visible suffix. One physical line
452
+ * may exceed the budget, but it is still kept so the newest change is visible.
453
+ */
454
+ function sliceStreamingDiffTail(diff: string, innerWidth: number, budget: number): StreamingDiffTail {
455
+ let end = diff.length;
456
+ while (end > 0 && diff.charCodeAt(end - 1) === 10) end--;
457
+ if (end === 0) return { content: "", hidden: false };
458
+
459
+ const rowLimit = Math.max(1, budget);
460
+ let start = end;
461
+ let cursor = end;
462
+ let visualRows = 0;
463
+ while (cursor >= 0) {
464
+ const newline = cursor > 0 ? diff.lastIndexOf("\n", cursor - 1) : -1;
465
+ const lineStart = newline + 1;
466
+ const lineRows = Math.max(1, wrapTextWithAnsi(replaceTabs(diff.slice(lineStart, cursor)), innerWidth).length);
467
+ if (visualRows > 0 && visualRows + lineRows > rowLimit) break;
468
+ visualRows += lineRows;
469
+ start = lineStart;
470
+ if (newline < 0) break;
471
+ cursor = newline;
472
+ }
473
+
474
+ return { content: diff.slice(start, end), hidden: start > 0 };
475
+ }
476
+
477
+ function formatStreamingDiff(
478
+ diff: string,
479
+ rawPath: string,
480
+ width: number,
481
+ uiTheme: Theme,
482
+ expanded: boolean,
483
+ label = "streaming",
484
+ spinnerFrame?: number,
485
+ cache?: RenderedStringCache,
486
+ ): string {
487
+ if (!diff) return "";
488
+ // Clamp the tail to the viewport so a tall or fast-growing diff cannot
489
+ // outgrow the live window. Otherwise its mutating rows scroll above the
490
+ // native-scrollback commit boundary mid-stream and freeze into immutable
491
+ // history as a stale preview snapshot; the finalize repair then recommits
492
+ // the final render below it — a duplicated block on the tape. Collapsed
493
+ // gets a short fixed tail; expanded widens it to the viewport-sized window,
494
+ // never unbounded. The budget is VISUAL rows (a long wrapped line counts
495
+ // for more than one) at the framed block's inner width (border only —
496
+ // contentPaddingLeft is 0); only the visible suffix is syntax-colored, so
497
+ // the cheap raw-line wrap walk keeps the per-chunk cost bounded.
498
+ // innerWidth/budget are in the cache salt so a resize re-slices.
499
+ const innerWidth = Math.max(1, width - 2);
500
+ const budget = expanded ? previewWindowRows() : Math.min(PREVIEW_LIMITS.EXPANDED_LINES, previewWindowRows());
501
+ let text = cachedRenderedString(cache, uiTheme, expanded, `${rawPath}:${innerWidth}:${budget}`, diff, () => {
502
+ // "Cursor" tail window: pin the last rows to the bottom so freshly streamed
503
+ // changes stay on screen. The whole-file diff is recomputed every chunk and
504
+ // its Myers alignment is not monotonic in payload length, so a hunk-aware
505
+ // window stutters as rows move between hunks. Expanded widens the window
506
+ // to the viewport; the full diff appears once the result finalizes.
507
+ const tail = sliceStreamingDiffTail(diff, innerWidth, budget);
508
+ let rendered = "\n\n";
509
+ if (tail.hidden) {
510
+ // Exact hidden line/hunk counts require scanning the discarded prefix,
511
+ // which would make every streaming update scale with the complete diff.
512
+ rendered += `${uiTheme.fg("dim", "… (content above)")}\n`;
513
+ }
514
+ rendered += renderDiffColored(tail.content, { filePath: rawPath, theme: uiTheme });
515
+ return rendered;
516
+ });
517
+ // The animated glyph rides this trailing line — inside the transcript's
518
+ // volatile-tail holdback — never the block header: an animating head row
519
+ // pins the native-scrollback commit boundary at the top of the block, so a
520
+ // tall expanded preview could never scroll-append mid-stream.
521
+ const spinner = spinnerFrame !== undefined ? `${formatStatusIcon("running", uiTheme, spinnerFrame)} ` : "";
522
+ // Expanded approval previews hide the "(preview)" label (#1992) but keep
523
+ // the animated glyph when one is active so the volatile tail stays live.
524
+ const hideLabel = expanded && label === "preview";
525
+ if (spinner || !hideLabel) {
526
+ text += `\n${hideLabel ? spinner.trimEnd() : `${spinner}${uiTheme.fg("dim", `(${label})`)}`}`;
527
+ }
528
+ return text;
529
+ }
530
+
531
+ function formatMultiFileStreamingDiff(
532
+ previews: PerFileDiffPreview[],
533
+ width: number,
534
+ uiTheme: Theme,
535
+ expanded: boolean,
536
+ spinnerFrame?: number,
537
+ caches?: RenderedStringCache[],
538
+ ): string {
539
+ const parts: string[] = [];
540
+ for (let index = 0; index < previews.length; index++) {
541
+ const preview = previews[index]!;
542
+ if (!preview.diff && !preview.error) continue;
543
+ const header = uiTheme.fg("dim", `\n\n── ${shortenPath(preview.path)} ──`);
544
+ if (preview.error) {
545
+ parts.push(`${header}\n${uiTheme.fg("error", replaceTabs(preview.error))}`);
546
+ continue;
547
+ }
548
+ if (preview.diff) {
549
+ // Only the last file's preview carries the animated streaming glyph;
550
+ // earlier files have settled and must stay byte-stable so their rows
551
+ // can commit to native scrollback mid-stream.
552
+ const isLast = index === previews.length - 1;
553
+ const cache = previewCacheAt(caches, index);
554
+ parts.push(
555
+ `${header}${formatStreamingDiff(preview.diff, preview.path, width, uiTheme, expanded, "preview", isLast ? spinnerFrame : undefined, cache)}`,
556
+ );
557
+ }
558
+ }
559
+ return parts.join("");
560
+ }
561
+
562
+ function getCallPreview(
563
+ args: EditRenderArgs,
564
+ rawPath: string,
565
+ width: number,
566
+ uiTheme: Theme,
567
+ renderContext: EditRenderContext | undefined,
568
+ expanded: boolean,
569
+ spinnerFrame?: number,
570
+ caches?: RenderedStringCache[],
571
+ ): string {
572
+ const multi = renderContext?.perFileDiffPreview;
573
+ if (multi && multi.length > 1 && multi.some(p => p.diff || p.error)) {
574
+ return formatMultiFileStreamingDiff(multi, width, uiTheme, expanded, spinnerFrame, caches);
575
+ }
576
+ const cache = previewCacheAt(caches, 0);
577
+ if (args.previewDiff) {
578
+ return formatStreamingDiff(args.previewDiff, rawPath, width, uiTheme, expanded, "preview", spinnerFrame, cache);
579
+ }
580
+ if (args.diff && args.op) {
581
+ return formatStreamingDiff(args.diff, rawPath, width, uiTheme, expanded, "streaming", spinnerFrame, cache);
582
+ }
583
+ if (args.diff) {
584
+ return renderPlainTextPreview(args.diff, uiTheme, rawPath);
585
+ }
586
+ if (args.newText || args.patch) {
587
+ return renderPlainTextPreview(args.newText ?? args.patch ?? "", uiTheme, rawPath);
588
+ }
589
+ if (renderContext?.editStreamingFallback) {
590
+ return renderContext.editStreamingFallback;
591
+ }
592
+ const contextPreview = renderContext?.editDiffPreview;
593
+ if (contextPreview && "error" in contextPreview && contextPreview.error) {
594
+ return uiTheme.fg("error", replaceTabs(contextPreview.error));
595
+ }
596
+ return "";
597
+ }
598
+
599
+ const MISSING_APPLY_PATCH_END_ERROR = "The last line of the patch must be '*** End Patch'";
600
+
601
+ function normalizeHashlineInputPreviewPath(rawPath: string): string {
602
+ const trimmed = rawPath.trim();
603
+ const hashStart = /#[0-9a-fA-F]{4}$/u.exec(trimmed)?.index;
604
+ const withoutHash = hashStart === undefined ? trimmed : trimmed.slice(0, hashStart);
605
+ if (withoutHash.length < 2) return withoutHash;
606
+ const first = withoutHash[0];
607
+ const last = withoutHash[withoutHash.length - 1];
608
+ if ((first === '"' || first === "'") && first === last) {
609
+ return withoutHash.slice(1, -1);
610
+ }
611
+ return withoutHash;
612
+ }
613
+
614
+ function parseHashlineInputPreviewHeader(line: string): string | null {
615
+ const trimmed = line.trimEnd();
616
+ if (!trimmed.startsWith(HL_FILE_PREFIX)) return null;
617
+ // Keep streaming previews tolerant while the closing bracket is still
618
+ // being generated; the parser enforces the final `[path#TAG]` shape.
619
+ const bodyEnd = trimmed.endsWith(HL_FILE_SUFFIX) ? trimmed.length - HL_FILE_SUFFIX.length : trimmed.length;
620
+ const body = trimmed.slice(HL_FILE_PREFIX.length, bodyEnd).trim();
621
+ const previewPath = normalizeHashlineInputPreviewPath(body);
622
+ return previewPath.length > 0 ? previewPath : null;
623
+ }
624
+
625
+ // Line-editing op headers (PUT/CUT family), distinct from file-level
626
+ // REM/MV ops. Body rows are `+TEXT`, so this only matches real headers.
627
+ const HL_LINE_OP_HEADER = /^(?:PUT|CUT)\b/;
628
+
629
+ /**
630
+ * Walk a (possibly mid-stream) hashline payload into per-section descriptors:
631
+ * the target path plus any file-level op (`REM` → delete, `MV dest` → rename)
632
+ * and whether a line edit precedes it. Tolerant of partial input so the call
633
+ * preview can label a delete/move before the payload finishes streaming.
634
+ */
635
+ function getHashlineInputSections(input: string): HashlineInputEntry[] {
636
+ const stripped = input.startsWith("\uFEFF") ? input.slice(1) : input;
637
+ const entries: HashlineInputEntry[] = [];
638
+ let current: HashlineInputEntry | undefined;
639
+ for (const rawLine of stripped.split("\n")) {
640
+ const line = rawLine.replace(/\r$/, "");
641
+ const headerPath = parseHashlineInputPreviewHeader(line);
642
+ if (headerPath) {
643
+ current = { path: headerPath };
644
+ entries.push(current);
645
+ continue;
646
+ }
647
+ if (!current) continue;
648
+ const trimmed = line.trim();
649
+ if (trimmed === HL_REM_KEYWORD) {
650
+ current.op = "delete";
651
+ } else if (trimmed.startsWith(`${HL_MOVE_KEYWORD} `)) {
652
+ current.rename = normalizeHashlineInputPreviewPath(trimmed.slice(HL_MOVE_KEYWORD.length + 1));
653
+ } else if (HL_LINE_OP_HEADER.test(trimmed)) {
654
+ current.hasLineEdits = true;
655
+ }
656
+ }
657
+ return entries;
658
+ }
659
+
660
+ function getHashlineInputRenderSummary(
661
+ args: EditRenderArgs,
662
+ editMode: EditMode | undefined,
663
+ ): HashlineInputRenderSummary | undefined {
664
+ const input = args.input ?? args._input;
665
+ if (editMode !== "hashline" || typeof input !== "string") {
666
+ return undefined;
667
+ }
668
+ return { entries: getHashlineInputSections(input) };
669
+ }
670
+
671
+ /**
672
+ * Per-file entries of a possibly partial payload. The native inspect re-parses
673
+ * the whole payload, and the call header and compact activity row both ask on
674
+ * every frame, so the answer (or the parser's rejection) is memoized on `args`
675
+ * for as long as `input` is unchanged.
676
+ */
677
+ function inspectInputEntries(args: EditRenderArgs, mode: EditMode, input: string): InspectedInputEntry[] {
678
+ const cached = args[kInspectedInput];
679
+ if (cached && cached.mode === mode && cached.input === input) {
680
+ if (cached.entries) return cached.entries;
681
+ throw cached.error;
682
+ }
683
+ let inspection: EditInspection;
684
+ try {
685
+ inspection = editInspect(mode, JSON.stringify({ input }));
686
+ } catch (error) {
687
+ args[kInspectedInput] = { mode, input, error };
688
+ throw error;
689
+ }
690
+ const entries = new Map<string, InspectedInputEntry>();
691
+ for (const path of inspection.paths) entries.set(path, { path });
692
+ for (const intent of inspection.fileOps) {
693
+ const entry = entries.get(intent.path) ?? { path: intent.path };
694
+ if (intent.kind === "delete") {
695
+ entry.op = "delete";
696
+ } else if (intent.kind === "move") {
697
+ entry.op = "update";
698
+ entry.rename = intent.to;
699
+ }
700
+ entries.set(intent.path, entry);
701
+ }
702
+ const result = [...entries.values()];
703
+ args[kInspectedInput] = { mode, input, entries: result };
704
+ return result;
705
+ }
706
+
707
+ /** Per-file descriptors for a possibly partial sloppy payload. */
708
+ function getSloppyInputRenderSummary(
709
+ args: EditRenderArgs,
710
+ editMode: EditMode | undefined,
711
+ ): { entries: InspectedInputEntry[] } | undefined {
712
+ const input = args.input ?? args._input;
713
+ if (editMode !== "sloppy" || typeof input !== "string") return undefined;
714
+ try {
715
+ const entries = inspectInputEntries(args, "sloppy", input);
716
+ return entries.length > 0 ? { entries } : undefined;
717
+ } catch {
718
+ return undefined;
719
+ }
720
+ }
721
+
722
+ function getApplyPatchRenderSummary(
723
+ args: EditRenderArgs,
724
+ isPartial: boolean,
725
+ editMode: EditMode | undefined,
726
+ ): ApplyPatchRenderSummary | undefined {
727
+ if ((editMode !== undefined && editMode !== "apply_patch") || typeof args.input !== "string") return undefined;
728
+ try {
729
+ return { entries: inspectInputEntries(args, "apply_patch", args.input) };
730
+ } catch (err) {
731
+ const error = err instanceof Error ? err.message : String(err);
732
+ return isPartial && error === MISSING_APPLY_PATCH_END_ERROR ? { entries: [] } : { entries: [], error };
733
+ }
734
+ }
735
+ /** Header facts (path, op, rename, file count) resolved from streamed edit args; shared by the framed call header and the compact activity summary. */
736
+ interface EditCallFacts {
737
+ rawPath: string;
738
+ rename?: string;
739
+ op?: Operation;
740
+ /** Distinct files touched by the call (0 when unknown). */
741
+ fileCount: number;
742
+ /** Apply-patch envelope parse error, when the payload failed to parse. */
743
+ applyPatchError?: string;
744
+ /** A hashline PUT/CUT line edit precedes the file op — keeps a move framed. */
745
+ hasHashlineLineEdits: boolean;
746
+ }
747
+
748
+ /**
749
+ * Length gate for the streamed-preview facts path: payloads only grow while
750
+ * streaming, so facts recompute only after K new bytes (the header/path/op
751
+ * fields tolerate a small lag) or when the payload shrinks (rewind). The
752
+ * final frame (`isPartial === false`) always recomputes. This turns the
753
+ * per-frame O(n) stringify+parse+scan into O(n^2/K) total per call.
754
+ */
755
+ const EDIT_FACTS_MIN_GROWTH = 512;
756
+ let lastFactsCache:
757
+ | {
758
+ editArgs: EditRenderArgs;
759
+ isPartial: boolean;
760
+ editMode: EditMode | undefined;
761
+ length: number;
762
+ facts: EditCallFacts;
763
+ }
764
+ | undefined;
765
+
766
+ function editFactsInputLength(editArgs: EditRenderArgs): number {
767
+ const input = editArgs.input ?? editArgs._input;
768
+ if (typeof input === "string") return input.length;
769
+ // Structured `edits[]` calls carry no `input` string; their facts derive
770
+ // from the array, so length-gate on its size instead of a constant 0
771
+ // (which would reuse forever after the first compute).
772
+ return Array.isArray(editArgs.edits) ? editArgs.edits.length : 0;
773
+ }
774
+
775
+ function resolveEditCallFacts(
776
+ editArgs: EditRenderArgs,
777
+ isPartial: boolean,
778
+ editMode: EditMode | undefined,
779
+ ): EditCallFacts {
780
+ const cached = lastFactsCache;
781
+ if (
782
+ cached !== undefined &&
783
+ cached.editArgs === editArgs &&
784
+ cached.isPartial === isPartial &&
785
+ cached.editMode === editMode
786
+ ) {
787
+ const length = editFactsInputLength(editArgs);
788
+ // Same args object, still growing gradually: reuse. A rewind
789
+ // (shorter), a jump past the gate, or the final frame recomputes.
790
+ if (isPartial && length >= cached.length && length - cached.length < EDIT_FACTS_MIN_GROWTH) {
791
+ return cached.facts;
792
+ }
793
+ }
794
+ const facts = resolveEditCallFactsUncached(editArgs, isPartial, editMode);
795
+ lastFactsCache = { editArgs, isPartial, editMode, length: editFactsInputLength(editArgs), facts };
796
+ return facts;
797
+ }
798
+
799
+ function resolveEditCallFactsUncached(
800
+ editArgs: EditRenderArgs,
801
+ isPartial: boolean,
802
+ editMode: EditMode | undefined,
803
+ ): EditCallFacts {
804
+ const hashlineInputSummary = getHashlineInputRenderSummary(editArgs, editMode);
805
+ const sloppyInputSummary = getSloppyInputRenderSummary(editArgs, editMode);
806
+ const applyPatchSummary = getApplyPatchRenderSummary(editArgs, isPartial, editMode);
807
+ const firstApplyPatchEntry = applyPatchSummary?.entries[0];
808
+ const firstHashlineInputEntry = hashlineInputSummary?.entries[0];
809
+ // Extract path from first edit entry when top-level path is absent (new schema)
810
+ const firstEdit = Array.isArray(editArgs.edits) && editArgs.edits.length > 0 ? editArgs.edits[0] : undefined;
811
+ const rawPath =
812
+ typeof editArgs.file_path === "string"
813
+ ? editArgs.file_path
814
+ : typeof editArgs.path === "string"
815
+ ? editArgs.path
816
+ : (filePathFromEditEntry(firstEdit?.path) ??
817
+ getPartialJsonEditPath(editArgs) ??
818
+ firstHashlineInputEntry?.path ??
819
+ sloppyInputSummary?.entries[0]?.path ??
820
+ firstApplyPatchEntry?.path ??
821
+ "");
822
+ const rename =
823
+ (typeof editArgs.rename === "string" ? editArgs.rename : undefined) ??
824
+ filePathFromEditEntry(firstEdit?.rename) ??
825
+ filePathFromEditEntry(firstEdit?.move) ??
826
+ firstApplyPatchEntry?.rename ??
827
+ firstHashlineInputEntry?.rename;
828
+ const op = editArgs.op || firstEdit?.op || firstApplyPatchEntry?.op || firstHashlineInputEntry?.op;
829
+ let fileCount =
830
+ hashlineInputSummary?.entries.length ??
831
+ sloppyInputSummary?.entries.length ??
832
+ applyPatchSummary?.entries.length ??
833
+ 0;
834
+ if (Array.isArray(editArgs.edits)) {
835
+ fileCount = countEditFiles(editArgs.edits);
836
+ }
837
+ return {
838
+ rawPath,
839
+ rename,
840
+ op,
841
+ fileCount,
842
+ applyPatchError: applyPatchSummary?.error,
843
+ hasHashlineLineEdits: Boolean(firstHashlineInputEntry?.hasLineEdits),
844
+ };
845
+ }
846
+
847
+ function formatDiffStatsSuffix(diff: string, uiTheme: Theme): string {
848
+ const { added, removed } = getDiffStats(diff);
849
+ if (added === 0 && removed === 0) return "";
850
+ const stats = [
851
+ added > 0 ? uiTheme.fg("toolDiffAdded", `+${added}`) : undefined,
852
+ removed > 0 ? uiTheme.fg("toolDiffRemoved", `-${removed}`) : undefined,
853
+ ].filter(value => value !== undefined);
854
+ return ` ${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${stats.join(uiTheme.fg("dim", "/"))}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
855
+ }
856
+ function renderDiffSection(
857
+ diff: string,
858
+ rawPath: string,
859
+ expanded: boolean,
860
+ innerWidth: number,
861
+ uiTheme: Theme,
862
+ renderDiffFn: (t: string, o?: { filePath?: string }) => string,
863
+ renderCache?: RenderedStringCache,
864
+ sectionCache?: RenderedStringCache,
865
+ ): string {
866
+ return cachedRenderedString(sectionCache, uiTheme, expanded, `${rawPath}:${innerWidth}`, diff, () => {
867
+ const {
868
+ text: truncatedDiff,
869
+ hiddenHunks,
870
+ hiddenLines: logicallyHiddenLines,
871
+ } = expanded
872
+ ? { text: diff, hiddenHunks: 0, hiddenLines: 0 }
873
+ : truncateDiffByHunk(diff, PREVIEW_LIMITS.DIFF_COLLAPSED_HUNKS, PREVIEW_LIMITS.DIFF_COLLAPSED_LINES);
874
+
875
+ const renderedDiff = cachedRenderedString(renderCache, uiTheme, expanded, rawPath, truncatedDiff, () =>
876
+ renderDiffFn(truncatedDiff, { filePath: rawPath }),
877
+ );
878
+ const { text: visibleDiff, hiddenLines: visuallyHiddenLines } = expanded
879
+ ? { text: renderedDiff, hiddenLines: 0 }
880
+ : sliceCollapsedDiffRows(renderedDiff, innerWidth, PREVIEW_LIMITS.DIFF_COLLAPSED_LINES);
881
+ const hiddenLines = logicallyHiddenLines + visuallyHiddenLines;
882
+
883
+ let text = `\n${visibleDiff}`;
884
+ if (!expanded && (hiddenHunks > 0 || hiddenLines > 0)) {
885
+ const remainder: string[] = [];
886
+ if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
887
+ if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
888
+ text += uiTheme.fg("toolOutput", `\n… (${remainder.join(", ")}) ${formatExpandHint(uiTheme)}`);
889
+ }
890
+ return text;
891
+ });
892
+ }
893
+
894
+ function wrapEditRendererLine(line: string, width: number): string[] {
895
+ if (width <= 0) return [line];
896
+ if (line.length === 0) return [""];
897
+
898
+ const startAnsi = line.match(/^((?:\x1b\[[0-9;]*m)*)/)?.[1] ?? "";
899
+ const bodyWithReset = line.slice(startAnsi.length);
900
+ const body = bodyWithReset.endsWith("\x1b[39m") ? bodyWithReset.slice(0, -"\x1b[39m".length) : bodyWithReset;
901
+ // Gutter shapes produced by formatCodeFrameLine: "-315│", " 313│", "+322│",
902
+ // plus the deduplicated forms " +│" and " │" whose repeated line number
903
+ // renderDiff blanked (single-line replacement pairs and insert-then-context
904
+ // runs) — all │-separated. ASCII "|" gutters may arrive from injected
905
+ // renderers that preserve raw canonical rows; those always carry a marker
906
+ // column ("+"/"-"/space) and a line number. So
907
+ // the number is optional for "│", while "|" requires the full canonical
908
+ // shape; anything else (a body line merely starting with "|", error text
909
+ // like "123|…") is not a diff row and wraps generically.
910
+ const diffMatch = /^(\s*[+-]?\s*\d*)([|│])(.*)$/s.exec(body);
911
+
912
+ if (!diffMatch || diffMatch[1].length === 0 || (diffMatch[2] === "|" && !/^[+\-\s]\s*\d+$/.test(diffMatch[1]))) {
913
+ return wrapTextWithAnsi(line, width);
914
+ }
915
+
916
+ const [, gutter, separator, content] = diffMatch;
917
+ const prefix = `${gutter}${separator}`;
918
+ const prefixWidth = visibleWidth(prefix);
919
+ const contentWidth = Math.max(1, width - prefixWidth);
920
+ const continuationPrefix = `${" ".repeat(Math.max(0, prefixWidth - 1))}${separator}`;
921
+ const wrappedContent = wrapTextWithAnsi(content ?? "", contentWidth);
922
+
923
+ // Each visual row is a standalone terminal line: wrapTextWithAnsi re-opens
924
+ // active SGR state at the next row's start, so a row that breaks inside an
925
+ // intra-line diff highlight still ends with inverse video active. Close it
926
+ // alongside the foreground reset — otherwise the frame padding appended
927
+ // after the row is painted as an inverse block (default-foreground cells).
928
+ return wrappedContent.map(
929
+ (segment, index) => `${startAnsi}${index === 0 ? prefix : continuationPrefix}${segment}\x1b[27m\x1b[39m`,
930
+ );
931
+ }
932
+
933
+ function sliceCollapsedDiffRows(
934
+ renderedDiff: string,
935
+ innerWidth: number,
936
+ maxRows: number,
937
+ ): { text: string; hiddenLines: number } {
938
+ const lines = renderedDiff.split("\n");
939
+ const visibleRows: string[] = [];
940
+ let completeLines = 0;
941
+
942
+ for (const line of lines) {
943
+ const wrapped = wrapEditRendererLine(line, innerWidth);
944
+ const remainingRows = maxRows - visibleRows.length;
945
+ if (wrapped.length <= remainingRows) {
946
+ visibleRows.push(...wrapped);
947
+ completeLines++;
948
+ continue;
949
+ }
950
+ if (remainingRows > 0) visibleRows.push(...wrapped.slice(0, remainingRows));
951
+ break;
952
+ }
953
+
954
+ return {
955
+ text: visibleRows.join("\n"),
956
+ hiddenLines: lines.length - completeLines,
957
+ };
958
+ }
959
+
960
+ /** Render edit previews and completed file diffs. */
961
+ export const editToolRenderer = {
962
+ mergeCallAndResult: true,
963
+ /** Compact one-line activity: operation + target path instead of the payload's first line. */
964
+ activitySummary(args: unknown, context: ToolActivityContext): ToolActivitySummary {
965
+ const editArgs = (args ?? {}) as EditRenderArgs;
966
+ const editMode = (context.renderContext as EditRenderContext | undefined)?.editMode;
967
+ const facts = resolveEditCallFacts(editArgs, context.isPartial, editMode);
968
+ const label = getOperationTitle(facts.op);
969
+ if (!facts.rawPath) return { label };
970
+ let detail = formatEditTitlePath(facts.rawPath);
971
+ if (facts.rename) detail += ` → ${formatEditTitlePath(facts.rename)}`;
972
+ if (facts.fileCount > 1) detail += ` (+${facts.fileCount - 1} more)`;
973
+ return { label, detail };
974
+ },
975
+
976
+ renderCall(
977
+ args: EditRenderArgs,
978
+ options: RenderResultOptions & { renderContext?: EditRenderContext },
979
+ uiTheme: Theme,
980
+ ): Component {
981
+ const renderContext = options.renderContext;
982
+ const editArgs = args as EditRenderArgs;
983
+ const { rawPath, rename, op, fileCount, applyPatchError, hasHashlineLineEdits } = resolveEditCallFacts(
984
+ editArgs,
985
+ options.isPartial,
986
+ renderContext?.editMode,
987
+ );
988
+ // Delete / payload-less move calls render as an inline pending row (no
989
+ // empty framed container), mirroring the completed result but with the
990
+ // shared hourglass instead of the eraser/move glyph.
991
+ const hasPayload = hasEditCallPayload(editArgs, renderContext) || hasHashlineLineEdits;
992
+ if (fileCount <= 1 && !applyPatchError && (op === "delete" || (rename !== undefined && !hasPayload))) {
993
+ return renderInlineEditRow(uiTheme, { op, rename, rawPath, pending: true });
994
+ }
995
+ const callPreviewCaches: RenderedStringCache[] = [];
996
+ return framedToolCard(uiTheme, ({ width }) => {
997
+ // No status icon on the head row: it's the head of the framed block,
998
+ // and native-scrollback commits are prefix-only — an animated glyph
999
+ // would pin the commit boundary at the top, and the pending hourglass
1000
+ // just adds noise. The liveness cue rides the trailing "(preview)" /
1001
+ // "(streaming)" line instead.
1002
+ const header = renderEditHeader(width, uiTheme, {
1003
+ op,
1004
+ rawPath,
1005
+ rename,
1006
+ extraSuffix: fileCount > 1 ? uiTheme.fg("dim", ` (+${fileCount - 1} more)`) : undefined,
1007
+ });
1008
+ let body = getCallPreview(
1009
+ editArgs,
1010
+ rawPath,
1011
+ width,
1012
+ uiTheme,
1013
+ renderContext,
1014
+ options.expanded,
1015
+ options?.spinnerFrame,
1016
+ callPreviewCaches,
1017
+ );
1018
+ if (applyPatchError) {
1019
+ body += `\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchError), Math.max(1, width - 2)))}`;
1020
+ }
1021
+ const bodyLines = body ? body.split("\n") : [];
1022
+ while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
1023
+ return {
1024
+ header,
1025
+ sections: bodyLines.length > 0 ? [{ content: bodyLines }] : [],
1026
+ phase: applyPatchError ? "error" : "pending",
1027
+ borderColor: applyPatchError ? "error" : "borderMuted",
1028
+ contentPaddingLeft: 0,
1029
+ };
1030
+ });
1031
+ },
1032
+
1033
+ renderResult(
1034
+ result: { content: Array<{ type: string; text?: string }>; details?: EditToolDetails; isError?: boolean },
1035
+ options: RenderResultOptions & { renderContext?: EditRenderContext },
1036
+ uiTheme: Theme,
1037
+ args?: EditRenderArgs,
1038
+ ): Component {
1039
+ const edits = Array.isArray(args?.edits) ? args.edits : undefined;
1040
+ const perFileResults = result.details?.perFileResults;
1041
+ const totalFiles = edits ? countEditFiles(edits) : 0;
1042
+ if (perFileResults && (perFileResults.length > 1 || totalFiles > 1)) {
1043
+ return renderMultiFileResult(perFileResults, totalFiles, options, uiTheme);
1044
+ }
1045
+ return renderSingleFileResult(result, options, uiTheme, args);
1046
+ },
1047
+ } satisfies ToolRenderer<EditRenderArgs, EditToolDetails>;
1048
+
1049
+ function renderSingleFileResult(
1050
+ result: {
1051
+ content: Array<{ type: string; text?: string }>;
1052
+ details?: EditToolDetails | EditToolPerFileResult;
1053
+ isError?: boolean;
1054
+ },
1055
+ options: RenderResultOptions & { renderContext?: EditRenderContext },
1056
+ uiTheme: Theme,
1057
+ args?: EditRenderArgs,
1058
+ ): Component {
1059
+ const details = result.details;
1060
+ const isError = result.isError ?? (details && "isError" in details ? details.isError : false);
1061
+ const edits = Array.isArray(args?.edits) ? args.edits : undefined;
1062
+ const firstEdit = edits?.[0];
1063
+ const hashlineInputSummary = getHashlineInputRenderSummary(args ?? {}, options.renderContext?.editMode);
1064
+ const firstHashlineInputEntry = hashlineInputSummary?.entries[0];
1065
+ const moveSource =
1066
+ details && "sourcePath" in details && typeof details.sourcePath === "string" ? details.sourcePath : undefined;
1067
+ const detailPath = details && "path" in details && typeof details.path === "string" ? details.path : undefined;
1068
+ const rawPath =
1069
+ moveSource ??
1070
+ (typeof args?.file_path === "string"
1071
+ ? args.file_path
1072
+ : typeof args?.path === "string"
1073
+ ? args.path
1074
+ : (filePathFromEditEntry(firstEdit?.path) ?? detailPath ?? firstHashlineInputEntry?.path ?? ""));
1075
+ const op = args?.op || firstEdit?.op || details?.op;
1076
+ const rename =
1077
+ (typeof args?.rename === "string" ? args.rename : undefined) ??
1078
+ filePathFromEditEntry(firstEdit?.rename) ??
1079
+ filePathFromEditEntry(firstEdit?.move) ??
1080
+ (details && "move" in details && typeof details.move === "string" ? details.move : undefined);
1081
+
1082
+ const displayErrorText = isError && details && "displayErrorText" in details ? details.displayErrorText : undefined;
1083
+ const errorText = isError
1084
+ ? displayErrorText ||
1085
+ (details && "errorText" in details && details.errorText) ||
1086
+ (result.content?.find(c => c.type === "text")?.text ?? "")
1087
+ : "";
1088
+
1089
+ // Delete and move-only results carry no diff to box. Per design these render
1090
+ // as an inline status row (eraser / move glyph) rather than an empty framed
1091
+ // container. Errors, no-ops, creates, move-with-edits, and anything with
1092
+ // diagnostics keep the framed block below.
1093
+ if (!isError && !details?.diff && !details?.diagnostics && (op === "delete" || rename)) {
1094
+ const linkPath = details && "path" in details ? details.path : undefined;
1095
+ return renderInlineEditRow(uiTheme, { op, rename, rawPath, linkPath, pending: false });
1096
+ }
1097
+
1098
+ const renderFallbackDiff = (diffText: string, diffOptions?: { filePath?: string }): string =>
1099
+ renderDiffColored(diffText, { filePath: diffOptions?.filePath, theme: uiTheme });
1100
+
1101
+ let diffSectionRenderDiffFn: ((t: string, o?: { filePath?: string }) => string) | undefined;
1102
+ const diffSectionCache = createRenderedStringCache();
1103
+ const renderedDiffCache = createRenderedStringCache();
1104
+ const statsSuffixCache = createRenderedStringCache();
1105
+
1106
+ return framedToolCard(uiTheme, ({ width }) => {
1107
+ const { expanded, renderContext } = options;
1108
+ // A finalized result is authoritative: its `details` describe exactly
1109
+ // what happened. The shared streaming `editDiffPreview` is a call-phase
1110
+ // artifact (in a batch it reflects only the first file), so consulting it
1111
+ // for an empty-diff delete/move/no-op result mislabels the card. Fall
1112
+ // back to the preview only when no details exist yet.
1113
+ const editDiffPreview = details ? undefined : renderContext?.editDiffPreview;
1114
+ const renderDiffFn = renderContext?.renderDiff ?? renderFallbackDiff;
1115
+
1116
+ if (diffSectionRenderDiffFn !== renderDiffFn) {
1117
+ diffSectionRenderDiffFn = renderDiffFn;
1118
+ invalidateRenderedStringCache(diffSectionCache);
1119
+ invalidateRenderedStringCache(renderedDiffCache);
1120
+ }
1121
+ const firstChangedLine =
1122
+ (editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
1123
+ (details && !isError ? details.firstChangedLine : undefined);
1124
+ const linkPath = details && "path" in details ? details.path : undefined;
1125
+
1126
+ // Change stats ride inline on the header bar next to the path.
1127
+ const previewDiff = editDiffPreview && !("error" in editDiffPreview) ? editDiffPreview.diff : undefined;
1128
+ const headerDiff = isError ? undefined : details?.diff || previewDiff;
1129
+ const statsSuffix = headerDiff
1130
+ ? cachedRenderedString(statsSuffixCache, uiTheme, false, "", headerDiff, () =>
1131
+ formatDiffStatsSuffix(headerDiff, uiTheme),
1132
+ )
1133
+ : "";
1134
+ const header = renderEditHeader(width, uiTheme, {
1135
+ icon: isError ? "error" : "success",
1136
+ iconOverride: !isError && !options.isPartial ? uiTheme.styledSymbol("tool.edit", "accent") : undefined,
1137
+ op,
1138
+ rawPath,
1139
+ rename,
1140
+ firstChangedLine,
1141
+ linkPath,
1142
+ statsSuffix,
1143
+ });
1144
+ const innerWidth = Math.max(1, width - 2);
1145
+
1146
+ let body = "";
1147
+ if (isError) {
1148
+ if (errorText) body = uiTheme.fg("error", replaceTabs(errorText));
1149
+ } else if (details?.diff) {
1150
+ body = renderDiffSection(
1151
+ details.diff,
1152
+ rawPath,
1153
+ expanded,
1154
+ innerWidth,
1155
+ uiTheme,
1156
+ renderDiffFn,
1157
+ renderedDiffCache,
1158
+ diffSectionCache,
1159
+ );
1160
+ } else if (details) {
1161
+ // Authoritative result with no textual diff: a delete, a move-only
1162
+ // rename, or a genuine no-op. The header already names the op
1163
+ // (Delete / `src → dst`); only a true no-op needs an explanatory
1164
+ // body so an empty card isn't mistaken for a stalled edit.
1165
+ if (op !== "delete" && op !== "create" && !rename) {
1166
+ const noChangePath = linkPath ? shortenPath(linkPath) : rawPath ? shortenPath(rawPath) : "";
1167
+ body = uiTheme.fg("dim", `No changes were made${noChangePath ? ` to ${noChangePath}` : ""}.`);
1168
+ }
1169
+ } else if (editDiffPreview) {
1170
+ if ("error" in editDiffPreview) body = uiTheme.fg("error", replaceTabs(editDiffPreview.error));
1171
+ else if (editDiffPreview.diff)
1172
+ body = renderDiffSection(
1173
+ editDiffPreview.diff,
1174
+ rawPath,
1175
+ expanded,
1176
+ innerWidth,
1177
+ uiTheme,
1178
+ renderDiffFn,
1179
+ renderedDiffCache,
1180
+ diffSectionCache,
1181
+ );
1182
+ }
1183
+ if (details?.diagnostics) {
1184
+ body += formatDiagnostics(details.diagnostics, expanded, uiTheme, (fp: string) =>
1185
+ uiTheme.getLangIcon(getLanguageFromPath(fp)),
1186
+ );
1187
+ }
1188
+
1189
+ // Diff lines self-wrap with a continuation gutter; pre-wrap to the frame's
1190
+ // inner width so renderOutputBlock's generic wrap is a no-op. Edit frames
1191
+ // use a flush left border because code-frame gutters already provide padding.
1192
+ const bodyLines = body.length > 0 ? body.split("\n").flatMap(line => wrapEditRendererLine(line, innerWidth)) : [];
1193
+ while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
1194
+
1195
+ return {
1196
+ header,
1197
+ sections: bodyLines.length > 0 ? [{ content: bodyLines }] : [],
1198
+ phase: isError ? "error" : options.isPartial ? "partial" : "success",
1199
+ borderColor: isError ? "error" : "borderMuted",
1200
+ contentPaddingLeft: 0,
1201
+ };
1202
+ });
1203
+ }
1204
+
1205
+ function renderMultiFileResult(
1206
+ perFileResults: EditToolPerFileResult[],
1207
+ totalFiles: number,
1208
+ options: RenderResultOptions & { renderContext?: EditRenderContext },
1209
+ uiTheme: Theme,
1210
+ ): Component {
1211
+ const fileComponents = perFileResults.map(fileResult =>
1212
+ renderSingleFileResult({ content: [], details: fileResult, isError: fileResult.isError }, options, uiTheme),
1213
+ );
1214
+ const remaining = Math.max(0, totalFiles - perFileResults.length);
1215
+
1216
+ let cached: RenderCache | undefined;
1217
+
1218
+ return {
1219
+ render(width) {
1220
+ const key = new Hasher().bool(options.expanded).u32(width).u32(perFileResults.length).u32(remaining).digest();
1221
+ if (cached?.key === key) return cached.lines;
1222
+
1223
+ const allLines: string[] = [];
1224
+ for (let i = 0; i < fileComponents.length; i++) {
1225
+ if (i > 0) {
1226
+ allLines.push("");
1227
+ }
1228
+ allLines.push(...fileComponents[i].render(width));
1229
+ }
1230
+
1231
+ // Show pending indicator for files still being processed
1232
+ if (remaining > 0) {
1233
+ if (allLines.length > 0) allLines.push("");
1234
+ const spinnerFrame = options.spinnerFrame;
1235
+ const spinner = spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, spinnerFrame) : "";
1236
+ // Spinner while actively rendering, otherwise no icon — never the
1237
+ // pending hourglass on the head row.
1238
+ allLines.push(
1239
+ renderStatusLine(
1240
+ {
1241
+ iconOverride: spinner,
1242
+ title: "Edit",
1243
+ description: uiTheme.fg("dim", `${remaining} more file${remaining > 1 ? "s" : ""} pending…`),
1244
+ },
1245
+ uiTheme,
1246
+ ),
1247
+ );
1248
+ }
1249
+
1250
+ cached = { key, lines: allLines };
1251
+ return allLines;
1252
+ },
1253
+ invalidate() {
1254
+ cached = undefined;
1255
+ for (const c of fileComponents) c.invalidate?.();
1256
+ },
1257
+ };
1258
+ }