@draht/coding-agent 2026.3.14 → 2026.3.25-1

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 (345) hide show
  1. package/README.md +45 -30
  2. package/dist/bun/cli.d.ts +3 -0
  3. package/dist/bun/cli.d.ts.map +1 -0
  4. package/dist/bun/cli.js +7 -0
  5. package/dist/bun/cli.js.map +1 -0
  6. package/dist/bun/register-bedrock.d.ts +2 -0
  7. package/dist/bun/register-bedrock.d.ts.map +1 -0
  8. package/dist/bun/register-bedrock.js +4 -0
  9. package/dist/bun/register-bedrock.js.map +1 -0
  10. package/dist/cli/args.d.ts +1 -0
  11. package/dist/cli/args.d.ts.map +1 -1
  12. package/dist/cli/args.js +11 -6
  13. package/dist/cli/args.js.map +1 -1
  14. package/dist/cli/file-processor.d.ts.map +1 -1
  15. package/dist/cli/file-processor.js +4 -0
  16. package/dist/cli/file-processor.js.map +1 -1
  17. package/dist/cli/initial-message.d.ts +18 -0
  18. package/dist/cli/initial-message.d.ts.map +1 -0
  19. package/dist/cli/initial-message.js +22 -0
  20. package/dist/cli/initial-message.js.map +1 -0
  21. package/dist/cli/session-picker.d.ts.map +1 -1
  22. package/dist/cli/session-picker.js +2 -1
  23. package/dist/cli/session-picker.js.map +1 -1
  24. package/dist/cli.d.ts.map +1 -1
  25. package/dist/cli.js +1 -3
  26. package/dist/cli.js.map +1 -1
  27. package/dist/core/agent-session.d.ts +38 -5
  28. package/dist/core/agent-session.d.ts.map +1 -1
  29. package/dist/core/agent-session.js +201 -73
  30. package/dist/core/agent-session.js.map +1 -1
  31. package/dist/core/bash-executor.d.ts +6 -7
  32. package/dist/core/bash-executor.d.ts.map +1 -1
  33. package/dist/core/bash-executor.js +8 -107
  34. package/dist/core/bash-executor.js.map +1 -1
  35. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  36. package/dist/core/compaction/branch-summarization.js +1 -0
  37. package/dist/core/compaction/branch-summarization.js.map +1 -1
  38. package/dist/core/compaction/compaction.d.ts.map +1 -1
  39. package/dist/core/compaction/compaction.js +2 -0
  40. package/dist/core/compaction/compaction.js.map +1 -1
  41. package/dist/core/exec.d.ts.map +1 -1
  42. package/dist/core/exec.js +7 -3
  43. package/dist/core/exec.js.map +1 -1
  44. package/dist/core/export-html/index.d.ts +2 -2
  45. package/dist/core/export-html/index.d.ts.map +1 -1
  46. package/dist/core/export-html/index.js +7 -6
  47. package/dist/core/export-html/index.js.map +1 -1
  48. package/dist/core/export-html/template.css +43 -13
  49. package/dist/core/export-html/template.html +1 -0
  50. package/dist/core/export-html/template.js +107 -0
  51. package/dist/core/export-html/tool-renderer.d.ts +2 -2
  52. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  53. package/dist/core/export-html/tool-renderer.js +41 -16
  54. package/dist/core/export-html/tool-renderer.js.map +1 -1
  55. package/dist/core/extensions/index.d.ts +4 -3
  56. package/dist/core/extensions/index.d.ts.map +1 -1
  57. package/dist/core/extensions/index.js +1 -1
  58. package/dist/core/extensions/index.js.map +1 -1
  59. package/dist/core/extensions/loader.d.ts.map +1 -1
  60. package/dist/core/extensions/loader.js +16 -6
  61. package/dist/core/extensions/loader.js.map +1 -1
  62. package/dist/core/extensions/runner.d.ts +9 -9
  63. package/dist/core/extensions/runner.d.ts.map +1 -1
  64. package/dist/core/extensions/runner.js +89 -71
  65. package/dist/core/extensions/runner.js.map +1 -1
  66. package/dist/core/extensions/types.d.ts +49 -13
  67. package/dist/core/extensions/types.d.ts.map +1 -1
  68. package/dist/core/extensions/types.js.map +1 -1
  69. package/dist/core/extensions/wrapper.d.ts +4 -11
  70. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  71. package/dist/core/extensions/wrapper.js +6 -86
  72. package/dist/core/extensions/wrapper.js.map +1 -1
  73. package/dist/core/footer-data-provider.d.ts +13 -1
  74. package/dist/core/footer-data-provider.d.ts.map +1 -1
  75. package/dist/core/footer-data-provider.js +155 -37
  76. package/dist/core/footer-data-provider.js.map +1 -1
  77. package/dist/core/index.d.ts +2 -1
  78. package/dist/core/index.d.ts.map +1 -1
  79. package/dist/core/index.js +2 -1
  80. package/dist/core/index.js.map +1 -1
  81. package/dist/core/keybindings.d.ts +270 -50
  82. package/dist/core/keybindings.d.ts.map +1 -1
  83. package/dist/core/keybindings.js +222 -134
  84. package/dist/core/keybindings.js.map +1 -1
  85. package/dist/core/model-registry.d.ts +1 -0
  86. package/dist/core/model-registry.d.ts.map +1 -1
  87. package/dist/core/model-registry.js +49 -23
  88. package/dist/core/model-registry.js.map +1 -1
  89. package/dist/core/model-resolver.d.ts +6 -0
  90. package/dist/core/model-resolver.d.ts.map +1 -1
  91. package/dist/core/model-resolver.js +41 -17
  92. package/dist/core/model-resolver.js.map +1 -1
  93. package/dist/core/output-guard.d.ts +6 -0
  94. package/dist/core/output-guard.d.ts.map +1 -0
  95. package/dist/core/output-guard.js +59 -0
  96. package/dist/core/output-guard.js.map +1 -0
  97. package/dist/core/package-manager.d.ts +22 -1
  98. package/dist/core/package-manager.d.ts.map +1 -1
  99. package/dist/core/package-manager.js +373 -53
  100. package/dist/core/package-manager.js.map +1 -1
  101. package/dist/core/prompt-templates.d.ts +2 -1
  102. package/dist/core/prompt-templates.d.ts.map +1 -1
  103. package/dist/core/prompt-templates.js +39 -39
  104. package/dist/core/prompt-templates.js.map +1 -1
  105. package/dist/core/resolve-config-value.d.ts.map +1 -1
  106. package/dist/core/resolve-config-value.js +43 -8
  107. package/dist/core/resolve-config-value.js.map +1 -1
  108. package/dist/core/resource-loader.d.ts +6 -7
  109. package/dist/core/resource-loader.d.ts.map +1 -1
  110. package/dist/core/resource-loader.js +141 -118
  111. package/dist/core/resource-loader.js.map +1 -1
  112. package/dist/core/sdk.d.ts +3 -3
  113. package/dist/core/sdk.d.ts.map +1 -1
  114. package/dist/core/sdk.js +4 -4
  115. package/dist/core/sdk.js.map +1 -1
  116. package/dist/core/session-manager.d.ts +6 -0
  117. package/dist/core/session-manager.d.ts.map +1 -1
  118. package/dist/core/session-manager.js +9 -10
  119. package/dist/core/session-manager.js.map +1 -1
  120. package/dist/core/settings-manager.d.ts +3 -0
  121. package/dist/core/settings-manager.d.ts.map +1 -1
  122. package/dist/core/settings-manager.js +8 -0
  123. package/dist/core/settings-manager.js.map +1 -1
  124. package/dist/core/skills.d.ts +5 -3
  125. package/dist/core/skills.d.ts.map +1 -1
  126. package/dist/core/skills.js +54 -9
  127. package/dist/core/skills.js.map +1 -1
  128. package/dist/core/slash-commands.d.ts +2 -3
  129. package/dist/core/slash-commands.d.ts.map +1 -1
  130. package/dist/core/slash-commands.js +3 -2
  131. package/dist/core/slash-commands.js.map +1 -1
  132. package/dist/core/source-info.d.ts +18 -0
  133. package/dist/core/source-info.d.ts.map +1 -0
  134. package/dist/core/source-info.js +19 -0
  135. package/dist/core/source-info.js.map +1 -0
  136. package/dist/core/system-prompt.d.ts.map +1 -1
  137. package/dist/core/system-prompt.js +17 -60
  138. package/dist/core/system-prompt.js.map +1 -1
  139. package/dist/core/tools/bash.d.ts +24 -6
  140. package/dist/core/tools/bash.d.ts.map +1 -1
  141. package/dist/core/tools/bash.js +210 -110
  142. package/dist/core/tools/bash.js.map +1 -1
  143. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  144. package/dist/core/tools/edit-diff.js +1 -0
  145. package/dist/core/tools/edit-diff.js.map +1 -1
  146. package/dist/core/tools/edit.d.ts +14 -2
  147. package/dist/core/tools/edit.d.ts.map +1 -1
  148. package/dist/core/tools/edit.js +95 -23
  149. package/dist/core/tools/edit.js.map +1 -1
  150. package/dist/core/tools/file-mutation-queue.d.ts +6 -0
  151. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -0
  152. package/dist/core/tools/file-mutation-queue.js +37 -0
  153. package/dist/core/tools/file-mutation-queue.js.map +1 -0
  154. package/dist/core/tools/find.d.ts +11 -4
  155. package/dist/core/tools/find.d.ts.map +1 -1
  156. package/dist/core/tools/find.js +82 -30
  157. package/dist/core/tools/find.js.map +1 -1
  158. package/dist/core/tools/grep.d.ts +15 -4
  159. package/dist/core/tools/grep.d.ts.map +1 -1
  160. package/dist/core/tools/grep.js +83 -29
  161. package/dist/core/tools/grep.js.map +1 -1
  162. package/dist/core/tools/index.d.ts +58 -19
  163. package/dist/core/tools/index.d.ts.map +1 -1
  164. package/dist/core/tools/index.js +51 -26
  165. package/dist/core/tools/index.js.map +1 -1
  166. package/dist/core/tools/ls.d.ts +9 -3
  167. package/dist/core/tools/ls.d.ts.map +1 -1
  168. package/dist/core/tools/ls.js +67 -13
  169. package/dist/core/tools/ls.js.map +1 -1
  170. package/dist/core/tools/read.d.ts +10 -3
  171. package/dist/core/tools/read.d.ts.map +1 -1
  172. package/dist/core/tools/read.js +110 -51
  173. package/dist/core/tools/read.js.map +1 -1
  174. package/dist/core/tools/render-utils.d.ts +21 -0
  175. package/dist/core/tools/render-utils.d.ts.map +1 -0
  176. package/dist/core/tools/render-utils.js +49 -0
  177. package/dist/core/tools/render-utils.js.map +1 -0
  178. package/dist/core/tools/tool-definition-wrapper.d.ts +14 -0
  179. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -0
  180. package/dist/core/tools/tool-definition-wrapper.js +30 -0
  181. package/dist/core/tools/tool-definition-wrapper.js.map +1 -0
  182. package/dist/core/tools/write.d.ts +9 -3
  183. package/dist/core/tools/write.d.ts.map +1 -1
  184. package/dist/core/tools/write.js +168 -30
  185. package/dist/core/tools/write.js.map +1 -1
  186. package/dist/index.d.ts +5 -4
  187. package/dist/index.d.ts.map +1 -1
  188. package/dist/index.js +4 -3
  189. package/dist/index.js.map +1 -1
  190. package/dist/main.d.ts.map +1 -1
  191. package/dist/main.js +105 -226
  192. package/dist/main.js.map +1 -1
  193. package/dist/modes/interactive/components/bash-execution.d.ts +0 -1
  194. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  195. package/dist/modes/interactive/components/bash-execution.js +22 -9
  196. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  197. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  198. package/dist/modes/interactive/components/bordered-loader.js +1 -1
  199. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  200. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  201. package/dist/modes/interactive/components/branch-summary-message.js +2 -2
  202. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  203. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  204. package/dist/modes/interactive/components/compaction-summary-message.js +2 -2
  205. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  206. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  207. package/dist/modes/interactive/components/config-selector.js +8 -8
  208. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  209. package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
  210. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  211. package/dist/modes/interactive/components/custom-editor.js +6 -6
  212. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  213. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  214. package/dist/modes/interactive/components/extension-editor.js +9 -9
  215. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  216. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  217. package/dist/modes/interactive/components/extension-input.js +5 -5
  218. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  219. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  220. package/dist/modes/interactive/components/extension-selector.js +8 -8
  221. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  222. package/dist/modes/interactive/components/index.d.ts +1 -1
  223. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  224. package/dist/modes/interactive/components/index.js +1 -1
  225. package/dist/modes/interactive/components/index.js.map +1 -1
  226. package/dist/modes/interactive/components/keybinding-hints.d.ts +3 -36
  227. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  228. package/dist/modes/interactive/components/keybinding-hints.js +5 -44
  229. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  230. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  231. package/dist/modes/interactive/components/login-dialog.js +6 -6
  232. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  233. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  234. package/dist/modes/interactive/components/model-selector.js +13 -9
  235. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  236. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  237. package/dist/modes/interactive/components/oauth-selector.js +6 -6
  238. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  239. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  240. package/dist/modes/interactive/components/scoped-models-selector.js +4 -4
  241. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  242. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  243. package/dist/modes/interactive/components/session-selector.js +32 -35
  244. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  245. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  246. package/dist/modes/interactive/components/settings-selector.js +5 -1
  247. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  248. package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
  249. package/dist/modes/interactive/components/show-images-selector.js +5 -1
  250. package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
  251. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  252. package/dist/modes/interactive/components/skill-invocation-message.js +2 -2
  253. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  254. package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
  255. package/dist/modes/interactive/components/theme-selector.js +5 -1
  256. package/dist/modes/interactive/components/theme-selector.js.map +1 -1
  257. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  258. package/dist/modes/interactive/components/thinking-selector.js +5 -1
  259. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  260. package/dist/modes/interactive/components/tool-execution.d.ts +16 -34
  261. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  262. package/dist/modes/interactive/components/tool-execution.js +128 -636
  263. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  264. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  265. package/dist/modes/interactive/components/tree-selector.js +27 -16
  266. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  267. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  268. package/dist/modes/interactive/components/user-message-selector.js +6 -6
  269. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  270. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  271. package/dist/modes/interactive/components/user-message.js +2 -1
  272. package/dist/modes/interactive/components/user-message.js.map +1 -1
  273. package/dist/modes/interactive/interactive-mode.d.ts +7 -11
  274. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  275. package/dist/modes/interactive/interactive-mode.js +353 -214
  276. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  277. package/dist/modes/interactive/theme/theme.d.ts +3 -0
  278. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  279. package/dist/modes/interactive/theme/theme.js +63 -37
  280. package/dist/modes/interactive/theme/theme.js.map +1 -1
  281. package/dist/modes/print-mode.d.ts.map +1 -1
  282. package/dist/modes/print-mode.js +5 -11
  283. package/dist/modes/print-mode.js.map +1 -1
  284. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  285. package/dist/modes/rpc/rpc-mode.js +27 -17
  286. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  287. package/dist/modes/rpc/rpc-types.d.ts +3 -4
  288. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  289. package/dist/modes/rpc/rpc-types.js.map +1 -1
  290. package/dist/utils/child-process.d.ts +11 -0
  291. package/dist/utils/child-process.d.ts.map +1 -0
  292. package/dist/utils/child-process.js +78 -0
  293. package/dist/utils/child-process.js.map +1 -0
  294. package/dist/utils/clipboard-image.d.ts.map +1 -1
  295. package/dist/utils/clipboard-image.js +94 -11
  296. package/dist/utils/clipboard-image.js.map +1 -1
  297. package/dist/utils/clipboard-native.d.ts +1 -0
  298. package/dist/utils/clipboard-native.d.ts.map +1 -1
  299. package/dist/utils/clipboard-native.js.map +1 -1
  300. package/dist/utils/clipboard.d.ts +1 -1
  301. package/dist/utils/clipboard.d.ts.map +1 -1
  302. package/dist/utils/clipboard.js +27 -16
  303. package/dist/utils/clipboard.js.map +1 -1
  304. package/dist/utils/exif-orientation.d.ts +5 -0
  305. package/dist/utils/exif-orientation.d.ts.map +1 -0
  306. package/dist/utils/exif-orientation.js +158 -0
  307. package/dist/utils/exif-orientation.js.map +1 -0
  308. package/dist/utils/image-convert.d.ts.map +1 -1
  309. package/dist/utils/image-convert.js +5 -1
  310. package/dist/utils/image-convert.js.map +1 -1
  311. package/dist/utils/image-resize.d.ts +5 -5
  312. package/dist/utils/image-resize.d.ts.map +1 -1
  313. package/dist/utils/image-resize.js +51 -95
  314. package/dist/utils/image-resize.js.map +1 -1
  315. package/dist/utils/tools-manager.d.ts.map +1 -1
  316. package/dist/utils/tools-manager.js +5 -4
  317. package/dist/utils/tools-manager.js.map +1 -1
  318. package/docs/custom-provider.md +6 -2
  319. package/docs/extensions.md +108 -21
  320. package/docs/keybindings.md +103 -112
  321. package/docs/models.md +39 -1
  322. package/docs/packages.md +9 -0
  323. package/docs/providers.md +7 -0
  324. package/docs/rpc.md +15 -6
  325. package/docs/sdk.md +2 -2
  326. package/docs/settings.md +9 -0
  327. package/docs/terminal-setup.md +11 -0
  328. package/docs/tui.md +2 -2
  329. package/examples/extensions/README.md +2 -2
  330. package/examples/extensions/antigravity-image-gen.ts +5 -3
  331. package/examples/extensions/built-in-tool-renderer.ts +8 -8
  332. package/examples/extensions/commands.ts +3 -3
  333. package/examples/extensions/minimal-mode.ts +14 -14
  334. package/examples/extensions/question.ts +2 -2
  335. package/examples/extensions/questionnaire.ts +2 -2
  336. package/examples/extensions/subagent/index.ts +30 -8
  337. package/examples/extensions/titlebar-spinner.ts +2 -2
  338. package/examples/extensions/todo.ts +2 -2
  339. package/examples/extensions/tool-override.ts +9 -7
  340. package/examples/extensions/truncated-tool.ts +8 -5
  341. package/examples/sdk/04-skills.ts +8 -2
  342. package/examples/sdk/08-prompt-templates.ts +8 -2
  343. package/examples/sdk/12-full-control.ts +0 -1
  344. package/examples/sdk/README.md +1 -1
  345. package/package.json +4 -4
@@ -1,94 +1,47 @@
1
- import * as os from "node:os";
2
- import { Box, Container, getCapabilities, getImageDimensions, Image, imageFallback, Spacer, Text, truncateToWidth, } from "@draht/tui";
3
- import stripAnsi from "strip-ansi";
4
- import { computeEditDiff } from "../../../core/tools/edit-diff.js";
5
- import { allTools } from "../../../core/tools/index.js";
6
- import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/tools/truncate.js";
1
+ import { Box, Container, getCapabilities, Image, Spacer, Text } from "@draht/tui";
2
+ import { allToolDefinitions } from "../../../core/tools/index.js";
3
+ import { getTextOutput as getRenderedTextOutput } from "../../../core/tools/render-utils.js";
7
4
  import { convertToPng } from "../../../utils/image-convert.js";
8
- import { sanitizeBinaryOutput } from "../../../utils/shell.js";
9
- import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js";
10
- import { renderDiff } from "./diff.js";
11
- import { keyHint } from "./keybinding-hints.js";
12
- import { truncateToVisualLines } from "./visual-truncate.js";
13
- // Preview line limit for bash when not expanded
14
- const BASH_PREVIEW_LINES = 5;
15
- // During partial write tool-call streaming, re-highlight the first N lines fully
16
- // to keep multiline tokenization mostly correct without re-highlighting the full file.
17
- const WRITE_PARTIAL_FULL_HIGHLIGHT_LINES = 50;
18
- /**
19
- * Convert absolute path to tilde notation if it's in home directory
20
- */
21
- function shortenPath(path) {
22
- if (typeof path !== "string")
23
- return "";
24
- const home = os.homedir();
25
- if (path.startsWith(home)) {
26
- return `~${path.slice(home.length)}`;
27
- }
28
- return path;
29
- }
30
- /**
31
- * Replace tabs with spaces for consistent rendering
32
- */
33
- function replaceTabs(text) {
34
- return text.replace(/\t/g, " ");
35
- }
36
- /**
37
- * Normalize control characters for terminal preview rendering.
38
- * Keep tool arguments unchanged, sanitize only display text.
39
- */
40
- function normalizeDisplayText(text) {
41
- return text.replace(/\r/g, "");
42
- }
43
- /** Safely coerce value to string for display. Returns null if invalid type. */
44
- function str(value) {
45
- if (typeof value === "string")
46
- return value;
47
- if (value == null)
48
- return "";
49
- return null; // Invalid type
50
- }
51
- /**
52
- * Component that renders a tool call with its result (updateable)
53
- */
5
+ import { theme } from "../theme/theme.js";
54
6
  export class ToolExecutionComponent extends Container {
55
- contentBox; // Used for custom tools and bash visual truncation
56
- contentText; // For built-in tools (with its own padding/bg)
7
+ contentBox;
8
+ contentText;
9
+ callRendererComponent;
10
+ resultRendererComponent;
11
+ rendererState = {};
57
12
  imageComponents = [];
58
13
  imageSpacers = [];
59
14
  toolName;
15
+ toolCallId;
60
16
  args;
61
17
  expanded = false;
62
18
  showImages;
63
19
  isPartial = true;
64
20
  toolDefinition;
21
+ builtInToolDefinition;
65
22
  ui;
66
23
  cwd;
24
+ executionStarted = false;
25
+ argsComplete = false;
67
26
  result;
68
- // Cached edit diff preview (computed when args arrive, before tool executes)
69
- editDiffPreview;
70
- editDiffArgsKey; // Track which args the preview is for
71
- // Cached converted images for Kitty protocol (which requires PNG), keyed by index
72
27
  convertedImages = new Map();
73
- // Incremental syntax highlighting cache for write tool call args
74
- writeHighlightCache;
75
- // When true, this component intentionally renders no lines
76
28
  hideComponent = false;
77
- constructor(toolName, args, options = {}, toolDefinition, ui, cwd = process.cwd()) {
29
+ constructor(toolName, toolCallId, args, options = {}, toolDefinition, ui, cwd = process.cwd()) {
78
30
  super();
79
31
  this.toolName = toolName;
32
+ this.toolCallId = toolCallId;
80
33
  this.args = args;
81
- this.showImages = options.showImages ?? true;
82
34
  this.toolDefinition = toolDefinition;
35
+ this.builtInToolDefinition = allToolDefinitions[toolName];
36
+ this.showImages = options.showImages ?? true;
83
37
  this.ui = ui;
84
38
  this.cwd = cwd;
85
39
  this.addChild(new Spacer(1));
86
- // Always create both - contentBox for custom tools/bash, contentText for other built-ins
40
+ // Always create both. contentBox is used for tools with renderer-based call/result composition.
41
+ // contentText is reserved for generic fallback rendering when no tool definition exists.
87
42
  this.contentBox = new Box(1, 1, (text) => theme.bg("toolPendingBg", text));
88
43
  this.contentText = new Text("", 1, 1, (text) => theme.bg("toolPendingBg", text));
89
- // Use contentBox for bash (visual truncation) or custom tools with custom renderers
90
- // Use contentText for built-in tools (including overrides without custom renderers)
91
- if (toolName === "bash" || (toolDefinition && !this.shouldUseBuiltInRenderer())) {
44
+ if (this.hasRendererDefinition()) {
92
45
  this.addChild(this.contentBox);
93
46
  }
94
47
  else {
@@ -96,179 +49,96 @@ export class ToolExecutionComponent extends Container {
96
49
  }
97
50
  this.updateDisplay();
98
51
  }
99
- /**
100
- * Check if we should use built-in rendering for this tool.
101
- * Returns true if the tool name is a built-in AND either there's no toolDefinition
102
- * or the toolDefinition doesn't provide custom renderers.
103
- */
104
- shouldUseBuiltInRenderer() {
105
- const isBuiltInName = this.toolName in allTools;
106
- const hasCustomRenderers = this.toolDefinition?.renderCall || this.toolDefinition?.renderResult;
107
- return isBuiltInName && !hasCustomRenderers;
52
+ isBuiltInDefinition(definition) {
53
+ return (definition !== undefined &&
54
+ this.builtInToolDefinition !== undefined &&
55
+ definition.parameters === this.builtInToolDefinition.parameters);
108
56
  }
109
- updateArgs(args) {
110
- this.args = args;
111
- if (this.toolName === "write" && this.isPartial) {
112
- this.updateWriteHighlightCacheIncremental();
57
+ getCallRenderer() {
58
+ if (!this.builtInToolDefinition) {
59
+ return this.toolDefinition?.renderCall;
113
60
  }
114
- this.updateDisplay();
115
- }
116
- highlightSingleLine(line, lang) {
117
- const highlighted = highlightCode(line, lang);
118
- return highlighted[0] ?? "";
119
- }
120
- refreshWriteHighlightPrefix(cache) {
121
- const prefixCount = Math.min(WRITE_PARTIAL_FULL_HIGHLIGHT_LINES, cache.normalizedLines.length);
122
- if (prefixCount === 0)
123
- return;
124
- const prefixSource = cache.normalizedLines.slice(0, prefixCount).join("\n");
125
- const prefixHighlighted = highlightCode(prefixSource, cache.lang);
126
- for (let i = 0; i < prefixCount; i++) {
127
- cache.highlightedLines[i] =
128
- prefixHighlighted[i] ?? this.highlightSingleLine(cache.normalizedLines[i] ?? "", cache.lang);
61
+ if (!this.toolDefinition || this.isBuiltInDefinition(this.toolDefinition)) {
62
+ return this.builtInToolDefinition.renderCall;
129
63
  }
64
+ return this.toolDefinition.renderCall ?? this.builtInToolDefinition.renderCall;
130
65
  }
131
- rebuildWriteHighlightCacheFull(rawPath, fileContent) {
132
- const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
133
- if (!lang) {
134
- this.writeHighlightCache = undefined;
135
- return;
66
+ getResultRenderer() {
67
+ if (!this.builtInToolDefinition) {
68
+ return this.toolDefinition?.renderResult;
136
69
  }
137
- const displayContent = normalizeDisplayText(fileContent);
138
- const normalized = replaceTabs(displayContent);
139
- this.writeHighlightCache = {
140
- rawPath,
141
- lang,
142
- rawContent: fileContent,
143
- normalizedLines: normalized.split("\n"),
144
- highlightedLines: highlightCode(normalized, lang),
70
+ if (!this.toolDefinition || this.isBuiltInDefinition(this.toolDefinition)) {
71
+ return this.builtInToolDefinition.renderResult;
72
+ }
73
+ return this.toolDefinition.renderResult ?? this.builtInToolDefinition.renderResult;
74
+ }
75
+ hasRendererDefinition() {
76
+ return this.builtInToolDefinition !== undefined || this.toolDefinition !== undefined;
77
+ }
78
+ getRenderContext(lastComponent) {
79
+ return {
80
+ args: this.args,
81
+ toolCallId: this.toolCallId,
82
+ invalidate: () => {
83
+ this.invalidate();
84
+ this.ui.requestRender();
85
+ },
86
+ lastComponent,
87
+ state: this.rendererState,
88
+ cwd: this.cwd,
89
+ executionStarted: this.executionStarted,
90
+ argsComplete: this.argsComplete,
91
+ isPartial: this.isPartial,
92
+ expanded: this.expanded,
93
+ showImages: this.showImages,
94
+ isError: this.result?.isError ?? false,
145
95
  };
146
96
  }
147
- updateWriteHighlightCacheIncremental() {
148
- const rawPath = str(this.args?.file_path ?? this.args?.path);
149
- const fileContent = str(this.args?.content);
150
- if (rawPath === null || fileContent === null) {
151
- this.writeHighlightCache = undefined;
152
- return;
153
- }
154
- const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
155
- if (!lang) {
156
- this.writeHighlightCache = undefined;
157
- return;
158
- }
159
- if (!this.writeHighlightCache) {
160
- this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
161
- return;
162
- }
163
- const cache = this.writeHighlightCache;
164
- if (cache.lang !== lang || cache.rawPath !== rawPath) {
165
- this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
166
- return;
167
- }
168
- if (!fileContent.startsWith(cache.rawContent)) {
169
- this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
170
- return;
171
- }
172
- if (fileContent.length === cache.rawContent.length) {
173
- return;
174
- }
175
- const deltaRaw = fileContent.slice(cache.rawContent.length);
176
- const deltaDisplay = normalizeDisplayText(deltaRaw);
177
- const deltaNormalized = replaceTabs(deltaDisplay);
178
- cache.rawContent = fileContent;
179
- if (cache.normalizedLines.length === 0) {
180
- cache.normalizedLines.push("");
181
- cache.highlightedLines.push("");
182
- }
183
- const segments = deltaNormalized.split("\n");
184
- const lastIndex = cache.normalizedLines.length - 1;
185
- cache.normalizedLines[lastIndex] += segments[0];
186
- cache.highlightedLines[lastIndex] = this.highlightSingleLine(cache.normalizedLines[lastIndex], cache.lang);
187
- for (let i = 1; i < segments.length; i++) {
188
- cache.normalizedLines.push(segments[i]);
189
- cache.highlightedLines.push(this.highlightSingleLine(segments[i], cache.lang));
190
- }
191
- this.refreshWriteHighlightPrefix(cache);
97
+ createCallFallback() {
98
+ return new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0);
192
99
  }
193
- /**
194
- * Signal that args are complete (tool is about to execute).
195
- * This triggers diff computation for edit tool.
196
- */
197
- setArgsComplete() {
198
- if (this.toolName === "write") {
199
- const rawPath = str(this.args?.file_path ?? this.args?.path);
200
- const fileContent = str(this.args?.content);
201
- if (rawPath !== null && fileContent !== null) {
202
- this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
203
- }
100
+ createResultFallback() {
101
+ const output = this.getTextOutput();
102
+ if (!output) {
103
+ return undefined;
204
104
  }
205
- this.maybeComputeEditDiff();
105
+ return new Text(theme.fg("toolOutput", output), 0, 0);
206
106
  }
207
- /**
208
- * Compute edit diff preview when we have complete args.
209
- * This runs async and updates display when done.
210
- */
211
- maybeComputeEditDiff() {
212
- if (this.toolName !== "edit")
213
- return;
214
- const path = this.args?.path;
215
- const oldText = this.args?.oldText;
216
- const newText = this.args?.newText;
217
- // Need all three params to compute diff
218
- if (!path || oldText === undefined || newText === undefined)
219
- return;
220
- // Create a key to track which args this computation is for
221
- const argsKey = JSON.stringify({ path, oldText, newText });
222
- // Skip if we already computed for these exact args
223
- if (this.editDiffArgsKey === argsKey)
224
- return;
225
- this.editDiffArgsKey = argsKey;
226
- // Compute diff async
227
- computeEditDiff(path, oldText, newText, this.cwd).then((result) => {
228
- // Only update if args haven't changed since we started
229
- if (this.editDiffArgsKey === argsKey) {
230
- this.editDiffPreview = result;
231
- this.updateDisplay();
232
- this.ui.requestRender();
233
- }
234
- });
107
+ updateArgs(args) {
108
+ this.args = args;
109
+ this.updateDisplay();
110
+ }
111
+ markExecutionStarted() {
112
+ this.executionStarted = true;
113
+ this.updateDisplay();
114
+ this.ui.requestRender();
115
+ }
116
+ setArgsComplete() {
117
+ this.argsComplete = true;
118
+ this.updateDisplay();
119
+ this.ui.requestRender();
235
120
  }
236
121
  updateResult(result, isPartial = false) {
237
122
  this.result = result;
238
123
  this.isPartial = isPartial;
239
- if (this.toolName === "write" && !isPartial) {
240
- const rawPath = str(this.args?.file_path ?? this.args?.path);
241
- const fileContent = str(this.args?.content);
242
- if (rawPath !== null && fileContent !== null) {
243
- this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
244
- }
245
- }
246
124
  this.updateDisplay();
247
- // Convert non-PNG images to PNG for Kitty protocol (async)
248
125
  this.maybeConvertImagesForKitty();
249
126
  }
250
- /**
251
- * Convert non-PNG images to PNG for Kitty graphics protocol.
252
- * Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display.
253
- */
254
127
  maybeConvertImagesForKitty() {
255
128
  const caps = getCapabilities();
256
- // Only needed for Kitty protocol
257
129
  if (caps.images !== "kitty")
258
130
  return;
259
131
  if (!this.result)
260
132
  return;
261
- const imageBlocks = this.result.content?.filter((c) => c.type === "image") || [];
133
+ const imageBlocks = this.result.content.filter((c) => c.type === "image");
262
134
  for (let i = 0; i < imageBlocks.length; i++) {
263
135
  const img = imageBlocks[i];
264
136
  if (!img.data || !img.mimeType)
265
137
  continue;
266
- // Skip if already PNG or already converted
267
138
  if (img.mimeType === "image/png")
268
139
  continue;
269
140
  if (this.convertedImages.has(i))
270
141
  continue;
271
- // Convert async
272
142
  const index = i;
273
143
  convertToPng(img.data, img.mimeType).then((converted) => {
274
144
  if (converted) {
@@ -298,86 +168,66 @@ export class ToolExecutionComponent extends Container {
298
168
  return super.render(width);
299
169
  }
300
170
  updateDisplay() {
301
- // Set background based on state
302
171
  const bgFn = this.isPartial
303
172
  ? (text) => theme.bg("toolPendingBg", text)
304
173
  : this.result?.isError
305
174
  ? (text) => theme.bg("toolErrorBg", text)
306
175
  : (text) => theme.bg("toolSuccessBg", text);
307
- const useBuiltInRenderer = this.shouldUseBuiltInRenderer();
308
- let customRendererHasContent = false;
176
+ let hasContent = false;
309
177
  this.hideComponent = false;
310
- // Use built-in rendering for built-in tools (or overrides without custom renderers)
311
- if (useBuiltInRenderer) {
312
- if (this.toolName === "bash") {
313
- // Bash uses Box with visual line truncation
314
- this.contentBox.setBgFn(bgFn);
315
- this.contentBox.clear();
316
- this.renderBashContent();
317
- }
318
- else {
319
- // Other built-in tools: use Text directly with caching
320
- this.contentText.setCustomBgFn(bgFn);
321
- this.contentText.setText(this.formatToolExecution());
322
- }
323
- }
324
- else if (this.toolDefinition) {
325
- // Custom tools use Box for flexible component rendering
178
+ if (this.hasRendererDefinition()) {
326
179
  this.contentBox.setBgFn(bgFn);
327
180
  this.contentBox.clear();
328
- // Render call component
329
- if (this.toolDefinition.renderCall) {
181
+ const callRenderer = this.getCallRenderer();
182
+ if (!callRenderer) {
183
+ this.contentBox.addChild(this.createCallFallback());
184
+ hasContent = true;
185
+ }
186
+ else {
330
187
  try {
331
- const callComponent = this.toolDefinition.renderCall(this.args, theme);
332
- if (callComponent !== undefined) {
333
- this.contentBox.addChild(callComponent);
334
- customRendererHasContent = true;
335
- }
188
+ const component = callRenderer(this.args, theme, this.getRenderContext(this.callRendererComponent));
189
+ this.callRendererComponent = component;
190
+ this.contentBox.addChild(component);
191
+ hasContent = true;
336
192
  }
337
193
  catch {
338
- // Fall back to default on error
339
- this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0));
340
- customRendererHasContent = true;
194
+ this.callRendererComponent = undefined;
195
+ this.contentBox.addChild(this.createCallFallback());
196
+ hasContent = true;
341
197
  }
342
198
  }
343
- else {
344
- // No custom renderCall, show tool name
345
- this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0));
346
- customRendererHasContent = true;
347
- }
348
- // Render result component if we have a result
349
- if (this.result && this.toolDefinition.renderResult) {
350
- try {
351
- const resultComponent = this.toolDefinition.renderResult({ content: this.result.content, details: this.result.details }, { expanded: this.expanded, isPartial: this.isPartial }, theme);
352
- if (resultComponent !== undefined) {
353
- this.contentBox.addChild(resultComponent);
354
- customRendererHasContent = true;
199
+ if (this.result) {
200
+ const resultRenderer = this.getResultRenderer();
201
+ if (!resultRenderer) {
202
+ const component = this.createResultFallback();
203
+ if (component) {
204
+ this.contentBox.addChild(component);
205
+ hasContent = true;
355
206
  }
356
207
  }
357
- catch {
358
- // Fall back to showing raw output on error
359
- const output = this.getTextOutput();
360
- if (output) {
361
- this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
362
- customRendererHasContent = true;
208
+ else {
209
+ try {
210
+ const component = resultRenderer({ content: this.result.content, details: this.result.details }, { expanded: this.expanded, isPartial: this.isPartial }, theme, this.getRenderContext(this.resultRendererComponent));
211
+ this.resultRendererComponent = component;
212
+ this.contentBox.addChild(component);
213
+ hasContent = true;
214
+ }
215
+ catch {
216
+ this.resultRendererComponent = undefined;
217
+ const component = this.createResultFallback();
218
+ if (component) {
219
+ this.contentBox.addChild(component);
220
+ hasContent = true;
221
+ }
363
222
  }
364
- }
365
- }
366
- else if (this.result) {
367
- // Has result but no custom renderResult
368
- const output = this.getTextOutput();
369
- if (output) {
370
- this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
371
- customRendererHasContent = true;
372
223
  }
373
224
  }
374
225
  }
375
226
  else {
376
- // Unknown tool with no registered definition - show generic fallback
377
227
  this.contentText.setCustomBgFn(bgFn);
378
228
  this.contentText.setText(this.formatToolExecution());
229
+ hasContent = true;
379
230
  }
380
- // Handle images (same for both custom and built-in)
381
231
  for (const img of this.imageComponents) {
382
232
  this.removeChild(img);
383
233
  }
@@ -387,19 +237,16 @@ export class ToolExecutionComponent extends Container {
387
237
  }
388
238
  this.imageSpacers = [];
389
239
  if (this.result) {
390
- const imageBlocks = this.result.content?.filter((c) => c.type === "image") || [];
240
+ const imageBlocks = this.result.content.filter((c) => c.type === "image");
391
241
  const caps = getCapabilities();
392
242
  for (let i = 0; i < imageBlocks.length; i++) {
393
243
  const img = imageBlocks[i];
394
244
  if (caps.images && this.showImages && img.data && img.mimeType) {
395
- // Use converted PNG for Kitty protocol if available
396
245
  const converted = this.convertedImages.get(i);
397
246
  const imageData = converted?.data ?? img.data;
398
247
  const imageMimeType = converted?.mimeType ?? img.mimeType;
399
- // For Kitty, skip non-PNG images that haven't been converted yet
400
- if (caps.images === "kitty" && imageMimeType !== "image/png") {
248
+ if (caps.images === "kitty" && imageMimeType !== "image/png")
401
249
  continue;
402
- }
403
250
  const spacer = new Spacer(1);
404
251
  this.addChild(spacer);
405
252
  this.imageSpacers.push(spacer);
@@ -409,377 +256,22 @@ export class ToolExecutionComponent extends Container {
409
256
  }
410
257
  }
411
258
  }
412
- if (!useBuiltInRenderer && this.toolDefinition) {
413
- this.hideComponent = !customRendererHasContent && this.imageComponents.length === 0;
414
- }
415
- }
416
- /**
417
- * Render bash content using visual line truncation (like bash-execution.ts)
418
- */
419
- renderBashContent() {
420
- const command = str(this.args?.command);
421
- const timeout = this.args?.timeout;
422
- // Header
423
- const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
424
- const commandDisplay = command === null ? theme.fg("error", "[invalid arg]") : command ? command : theme.fg("toolOutput", "...");
425
- this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix, 0, 0));
426
- if (this.result) {
427
- const output = this.getTextOutput().trim();
428
- if (output) {
429
- // Style each line for the output
430
- const styledOutput = output
431
- .split("\n")
432
- .map((line) => theme.fg("toolOutput", line))
433
- .join("\n");
434
- if (this.expanded) {
435
- // Show all lines when expanded
436
- this.contentBox.addChild(new Text(`\n${styledOutput}`, 0, 0));
437
- }
438
- else {
439
- // Use visual line truncation when collapsed with width-aware caching
440
- let cachedWidth;
441
- let cachedLines;
442
- let cachedSkipped;
443
- this.contentBox.addChild({
444
- render: (width) => {
445
- if (cachedLines === undefined || cachedWidth !== width) {
446
- const result = truncateToVisualLines(styledOutput, BASH_PREVIEW_LINES, width);
447
- cachedLines = result.visualLines;
448
- cachedSkipped = result.skippedCount;
449
- cachedWidth = width;
450
- }
451
- if (cachedSkipped && cachedSkipped > 0) {
452
- const hint = theme.fg("muted", `... (${cachedSkipped} earlier lines,`) +
453
- ` ${keyHint("expandTools", "to expand")})`;
454
- return ["", truncateToWidth(hint, width, "..."), ...cachedLines];
455
- }
456
- // Add blank line for spacing (matches expanded case)
457
- return ["", ...cachedLines];
458
- },
459
- invalidate: () => {
460
- cachedWidth = undefined;
461
- cachedLines = undefined;
462
- cachedSkipped = undefined;
463
- },
464
- });
465
- }
466
- }
467
- // Truncation warnings
468
- const truncation = this.result.details?.truncation;
469
- const fullOutputPath = this.result.details?.fullOutputPath;
470
- if (truncation?.truncated || fullOutputPath) {
471
- const warnings = [];
472
- if (fullOutputPath) {
473
- warnings.push(`Full output: ${fullOutputPath}`);
474
- }
475
- if (truncation?.truncated) {
476
- if (truncation.truncatedBy === "lines") {
477
- warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
478
- }
479
- else {
480
- warnings.push(`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`);
481
- }
482
- }
483
- this.contentBox.addChild(new Text(`\n${theme.fg("warning", `[${warnings.join(". ")}]`)}`, 0, 0));
484
- }
259
+ if (this.hasRendererDefinition() && !hasContent && this.imageComponents.length === 0) {
260
+ this.hideComponent = true;
485
261
  }
486
262
  }
487
263
  getTextOutput() {
488
- if (!this.result)
489
- return "";
490
- const textBlocks = this.result.content?.filter((c) => c.type === "text") || [];
491
- const imageBlocks = this.result.content?.filter((c) => c.type === "image") || [];
492
- let output = textBlocks
493
- .map((c) => {
494
- // Use sanitizeBinaryOutput to handle binary data that crashes string-width
495
- return sanitizeBinaryOutput(stripAnsi(c.text || "")).replace(/\r/g, "");
496
- })
497
- .join("\n");
498
- const caps = getCapabilities();
499
- if (imageBlocks.length > 0 && (!caps.images || !this.showImages)) {
500
- const imageIndicators = imageBlocks
501
- .map((img) => {
502
- const dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;
503
- return imageFallback(img.mimeType, dims);
504
- })
505
- .join("\n");
506
- output = output ? `${output}\n${imageIndicators}` : imageIndicators;
507
- }
508
- return output;
264
+ return getRenderedTextOutput(this.result, this.showImages);
509
265
  }
510
266
  formatToolExecution() {
511
- let text = "";
512
- const invalidArg = theme.fg("error", "[invalid arg]");
513
- if (this.toolName === "read") {
514
- const rawPath = str(this.args?.file_path ?? this.args?.path);
515
- const path = rawPath !== null ? shortenPath(rawPath) : null;
516
- const offset = this.args?.offset;
517
- const limit = this.args?.limit;
518
- let pathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
519
- if (offset !== undefined || limit !== undefined) {
520
- const startLine = offset ?? 1;
521
- const endLine = limit !== undefined ? startLine + limit - 1 : "";
522
- pathDisplay += theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
523
- }
524
- text = `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}`;
525
- if (this.result) {
526
- const output = this.getTextOutput();
527
- const rawPath = str(this.args?.file_path ?? this.args?.path);
528
- const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
529
- const lines = lang ? highlightCode(replaceTabs(output), lang) : output.split("\n");
530
- const maxLines = this.expanded ? lines.length : 10;
531
- const displayLines = lines.slice(0, maxLines);
532
- const remaining = lines.length - maxLines;
533
- text +=
534
- "\n\n" +
535
- displayLines
536
- .map((line) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line))))
537
- .join("\n");
538
- if (remaining > 0) {
539
- text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`;
540
- }
541
- const truncation = this.result.details?.truncation;
542
- if (truncation?.truncated) {
543
- if (truncation.firstLineExceedsLimit) {
544
- text +=
545
- "\n" +
546
- theme.fg("warning", `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`);
547
- }
548
- else if (truncation.truncatedBy === "lines") {
549
- text +=
550
- "\n" +
551
- theme.fg("warning", `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`);
552
- }
553
- else {
554
- text +=
555
- "\n" +
556
- theme.fg("warning", `[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`);
557
- }
558
- }
559
- }
560
- }
561
- else if (this.toolName === "write") {
562
- const rawPath = str(this.args?.file_path ?? this.args?.path);
563
- const fileContent = str(this.args?.content);
564
- const path = rawPath !== null ? shortenPath(rawPath) : null;
565
- text =
566
- theme.fg("toolTitle", theme.bold("write")) +
567
- " " +
568
- (path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."));
569
- if (fileContent === null) {
570
- text += `\n\n${theme.fg("error", "[invalid content arg - expected string]")}`;
571
- }
572
- else if (fileContent) {
573
- const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
574
- let lines;
575
- if (lang) {
576
- const cache = this.writeHighlightCache;
577
- if (cache && cache.lang === lang && cache.rawPath === rawPath && cache.rawContent === fileContent) {
578
- lines = cache.highlightedLines;
579
- }
580
- else {
581
- const displayContent = normalizeDisplayText(fileContent);
582
- const normalized = replaceTabs(displayContent);
583
- lines = highlightCode(normalized, lang);
584
- this.writeHighlightCache = {
585
- rawPath,
586
- lang,
587
- rawContent: fileContent,
588
- normalizedLines: normalized.split("\n"),
589
- highlightedLines: lines,
590
- };
591
- }
592
- }
593
- else {
594
- lines = normalizeDisplayText(fileContent).split("\n");
595
- this.writeHighlightCache = undefined;
596
- }
597
- const totalLines = lines.length;
598
- const maxLines = this.expanded ? lines.length : 10;
599
- const displayLines = lines.slice(0, maxLines);
600
- const remaining = lines.length - maxLines;
601
- text +=
602
- "\n\n" +
603
- displayLines.map((line) => (lang ? line : theme.fg("toolOutput", replaceTabs(line)))).join("\n");
604
- if (remaining > 0) {
605
- text +=
606
- theme.fg("muted", `\n... (${remaining} more lines, ${totalLines} total,`) +
607
- ` ${keyHint("expandTools", "to expand")})`;
608
- }
609
- }
610
- // Show error if tool execution failed
611
- if (this.result?.isError) {
612
- const errorText = this.getTextOutput();
613
- if (errorText) {
614
- text += `\n\n${theme.fg("error", errorText)}`;
615
- }
616
- }
617
- }
618
- else if (this.toolName === "edit") {
619
- const rawPath = str(this.args?.file_path ?? this.args?.path);
620
- const path = rawPath !== null ? shortenPath(rawPath) : null;
621
- // Build path display, appending :line if we have diff info
622
- let pathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
623
- const firstChangedLine = (this.editDiffPreview && "firstChangedLine" in this.editDiffPreview
624
- ? this.editDiffPreview.firstChangedLine
625
- : undefined) ||
626
- (this.result && !this.result.isError ? this.result.details?.firstChangedLine : undefined);
627
- if (firstChangedLine) {
628
- pathDisplay += theme.fg("warning", `:${firstChangedLine}`);
629
- }
630
- text = `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`;
631
- if (this.result?.isError) {
632
- // Show error from result
633
- const errorText = this.getTextOutput();
634
- if (errorText) {
635
- text += `\n\n${theme.fg("error", errorText)}`;
636
- }
637
- }
638
- else if (this.result?.details?.diff) {
639
- // Tool executed successfully - use the diff from result
640
- // This takes priority over editDiffPreview which may have a stale error
641
- // due to race condition (async preview computed after file was modified)
642
- text += `\n\n${renderDiff(this.result.details.diff, { filePath: rawPath ?? undefined })}`;
643
- }
644
- else if (this.editDiffPreview) {
645
- // Use cached diff preview (before tool executes)
646
- if ("error" in this.editDiffPreview) {
647
- text += `\n\n${theme.fg("error", this.editDiffPreview.error)}`;
648
- }
649
- else if (this.editDiffPreview.diff) {
650
- text += `\n\n${renderDiff(this.editDiffPreview.diff, { filePath: rawPath ?? undefined })}`;
651
- }
652
- }
653
- }
654
- else if (this.toolName === "ls") {
655
- const rawPath = str(this.args?.path);
656
- const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
657
- const limit = this.args?.limit;
658
- text = `${theme.fg("toolTitle", theme.bold("ls"))} ${path === null ? invalidArg : theme.fg("accent", path)}`;
659
- if (limit !== undefined) {
660
- text += theme.fg("toolOutput", ` (limit ${limit})`);
661
- }
662
- if (this.result) {
663
- const output = this.getTextOutput().trim();
664
- if (output) {
665
- const lines = output.split("\n");
666
- const maxLines = this.expanded ? lines.length : 20;
667
- const displayLines = lines.slice(0, maxLines);
668
- const remaining = lines.length - maxLines;
669
- text += `\n\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
670
- if (remaining > 0) {
671
- text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`;
672
- }
673
- }
674
- const entryLimit = this.result.details?.entryLimitReached;
675
- const truncation = this.result.details?.truncation;
676
- if (entryLimit || truncation?.truncated) {
677
- const warnings = [];
678
- if (entryLimit) {
679
- warnings.push(`${entryLimit} entries limit`);
680
- }
681
- if (truncation?.truncated) {
682
- warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
683
- }
684
- text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
685
- }
686
- }
687
- }
688
- else if (this.toolName === "find") {
689
- const pattern = str(this.args?.pattern);
690
- const rawPath = str(this.args?.path);
691
- const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
692
- const limit = this.args?.limit;
693
- text =
694
- theme.fg("toolTitle", theme.bold("find")) +
695
- " " +
696
- (pattern === null ? invalidArg : theme.fg("accent", pattern || "")) +
697
- theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`);
698
- if (limit !== undefined) {
699
- text += theme.fg("toolOutput", ` (limit ${limit})`);
700
- }
701
- if (this.result) {
702
- const output = this.getTextOutput().trim();
703
- if (output) {
704
- const lines = output.split("\n");
705
- const maxLines = this.expanded ? lines.length : 20;
706
- const displayLines = lines.slice(0, maxLines);
707
- const remaining = lines.length - maxLines;
708
- text += `\n\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
709
- if (remaining > 0) {
710
- text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`;
711
- }
712
- }
713
- const resultLimit = this.result.details?.resultLimitReached;
714
- const truncation = this.result.details?.truncation;
715
- if (resultLimit || truncation?.truncated) {
716
- const warnings = [];
717
- if (resultLimit) {
718
- warnings.push(`${resultLimit} results limit`);
719
- }
720
- if (truncation?.truncated) {
721
- warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
722
- }
723
- text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
724
- }
725
- }
726
- }
727
- else if (this.toolName === "grep") {
728
- const pattern = str(this.args?.pattern);
729
- const rawPath = str(this.args?.path);
730
- const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
731
- const glob = str(this.args?.glob);
732
- const limit = this.args?.limit;
733
- text =
734
- theme.fg("toolTitle", theme.bold("grep")) +
735
- " " +
736
- (pattern === null ? invalidArg : theme.fg("accent", `/${pattern || ""}/`)) +
737
- theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`);
738
- if (glob) {
739
- text += theme.fg("toolOutput", ` (${glob})`);
740
- }
741
- if (limit !== undefined) {
742
- text += theme.fg("toolOutput", ` limit ${limit}`);
743
- }
744
- if (this.result) {
745
- const output = this.getTextOutput().trim();
746
- if (output) {
747
- const lines = output.split("\n");
748
- const maxLines = this.expanded ? lines.length : 15;
749
- const displayLines = lines.slice(0, maxLines);
750
- const remaining = lines.length - maxLines;
751
- text += `\n\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
752
- if (remaining > 0) {
753
- text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`;
754
- }
755
- }
756
- const matchLimit = this.result.details?.matchLimitReached;
757
- const truncation = this.result.details?.truncation;
758
- const linesTruncated = this.result.details?.linesTruncated;
759
- if (matchLimit || truncation?.truncated || linesTruncated) {
760
- const warnings = [];
761
- if (matchLimit) {
762
- warnings.push(`${matchLimit} matches limit`);
763
- }
764
- if (truncation?.truncated) {
765
- warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
766
- }
767
- if (linesTruncated) {
768
- warnings.push("some lines truncated");
769
- }
770
- text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
771
- }
772
- }
773
- }
774
- else {
775
- // Generic tool (shouldn't reach here for custom tools)
776
- text = theme.fg("toolTitle", theme.bold(this.toolName));
777
- const content = JSON.stringify(this.args, null, 2);
267
+ let text = theme.fg("toolTitle", theme.bold(this.toolName));
268
+ const content = JSON.stringify(this.args, null, 2);
269
+ if (content) {
778
270
  text += `\n\n${content}`;
779
- const output = this.getTextOutput();
780
- if (output) {
781
- text += `\n${output}`;
782
- }
271
+ }
272
+ const output = this.getTextOutput();
273
+ if (output) {
274
+ text += `\n${output}`;
783
275
  }
784
276
  return text;
785
277
  }