plugsuits 1.0.0

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 (560) hide show
  1. package/cli.js +3 -0
  2. package/dist/agent-reasoning-default.test.d.ts +2 -0
  3. package/dist/agent-reasoning-default.test.d.ts.map +1 -0
  4. package/dist/agent-reasoning-default.test.js +38 -0
  5. package/dist/agent-reasoning-default.test.js.map +1 -0
  6. package/dist/agent.d.ts +61 -0
  7. package/dist/agent.d.ts.map +1 -0
  8. package/dist/agent.js +308 -0
  9. package/dist/agent.js.map +1 -0
  10. package/dist/agent.test.d.ts +2 -0
  11. package/dist/agent.test.d.ts.map +1 -0
  12. package/dist/agent.test.js +38 -0
  13. package/dist/agent.test.js.map +1 -0
  14. package/dist/cli-args.d.ts +15 -0
  15. package/dist/cli-args.d.ts.map +1 -0
  16. package/dist/cli-args.js +63 -0
  17. package/dist/cli-args.js.map +1 -0
  18. package/dist/cli-args.test.d.ts +2 -0
  19. package/dist/cli-args.test.d.ts.map +1 -0
  20. package/dist/cli-args.test.js +105 -0
  21. package/dist/cli-args.test.js.map +1 -0
  22. package/dist/commands/aliases-and-tool-fallback.test.d.ts +2 -0
  23. package/dist/commands/aliases-and-tool-fallback.test.d.ts.map +1 -0
  24. package/dist/commands/aliases-and-tool-fallback.test.js +132 -0
  25. package/dist/commands/aliases-and-tool-fallback.test.js.map +1 -0
  26. package/dist/commands/clear.d.ts +3 -0
  27. package/dist/commands/clear.d.ts.map +1 -0
  28. package/dist/commands/clear.js +12 -0
  29. package/dist/commands/clear.js.map +1 -0
  30. package/dist/commands/factories/create-toggle-command.d.ts +13 -0
  31. package/dist/commands/factories/create-toggle-command.d.ts.map +1 -0
  32. package/dist/commands/factories/create-toggle-command.js +38 -0
  33. package/dist/commands/factories/create-toggle-command.js.map +1 -0
  34. package/dist/commands/help.d.ts +3 -0
  35. package/dist/commands/help.d.ts.map +1 -0
  36. package/dist/commands/help.js +23 -0
  37. package/dist/commands/help.js.map +1 -0
  38. package/dist/commands/index.d.ts +18 -0
  39. package/dist/commands/index.d.ts.map +1 -0
  40. package/dist/commands/index.js +82 -0
  41. package/dist/commands/index.js.map +1 -0
  42. package/dist/commands/model.d.ts +15 -0
  43. package/dist/commands/model.d.ts.map +1 -0
  44. package/dist/commands/model.js +100 -0
  45. package/dist/commands/model.js.map +1 -0
  46. package/dist/commands/reasoning-mode.d.ts +3 -0
  47. package/dist/commands/reasoning-mode.d.ts.map +1 -0
  48. package/dist/commands/reasoning-mode.js +47 -0
  49. package/dist/commands/reasoning-mode.js.map +1 -0
  50. package/dist/commands/render.d.ts +19 -0
  51. package/dist/commands/render.d.ts.map +1 -0
  52. package/dist/commands/render.js +140 -0
  53. package/dist/commands/render.js.map +1 -0
  54. package/dist/commands/render.test.d.ts +2 -0
  55. package/dist/commands/render.test.d.ts.map +1 -0
  56. package/dist/commands/render.test.js +36 -0
  57. package/dist/commands/render.test.js.map +1 -0
  58. package/dist/commands/tool-fallback.d.ts +3 -0
  59. package/dist/commands/tool-fallback.d.ts.map +1 -0
  60. package/dist/commands/tool-fallback.js +38 -0
  61. package/dist/commands/tool-fallback.js.map +1 -0
  62. package/dist/commands/translate.d.ts +3 -0
  63. package/dist/commands/translate.d.ts.map +1 -0
  64. package/dist/commands/translate.js +12 -0
  65. package/dist/commands/translate.js.map +1 -0
  66. package/dist/commands/translate.test.d.ts +2 -0
  67. package/dist/commands/translate.test.d.ts.map +1 -0
  68. package/dist/commands/translate.test.js +49 -0
  69. package/dist/commands/translate.test.js.map +1 -0
  70. package/dist/commands/types.d.ts +17 -0
  71. package/dist/commands/types.d.ts.map +1 -0
  72. package/dist/commands/types.js +2 -0
  73. package/dist/commands/types.js.map +1 -0
  74. package/dist/context/environment-context.d.ts +2 -0
  75. package/dist/context/environment-context.d.ts.map +1 -0
  76. package/dist/context/environment-context.js +11 -0
  77. package/dist/context/environment-context.js.map +1 -0
  78. package/dist/context/paths.d.ts +3 -0
  79. package/dist/context/paths.d.ts.map +1 -0
  80. package/dist/context/paths.js +3 -0
  81. package/dist/context/paths.js.map +1 -0
  82. package/dist/context/session.d.ts +4 -0
  83. package/dist/context/session.d.ts.map +1 -0
  84. package/dist/context/session.js +16 -0
  85. package/dist/context/session.js.map +1 -0
  86. package/dist/context/skill-command-prefix.d.ts +4 -0
  87. package/dist/context/skill-command-prefix.d.ts.map +1 -0
  88. package/dist/context/skill-command-prefix.js +15 -0
  89. package/dist/context/skill-command-prefix.js.map +1 -0
  90. package/dist/context/skills-integration.test.d.ts +2 -0
  91. package/dist/context/skills-integration.test.d.ts.map +1 -0
  92. package/dist/context/skills-integration.test.js +87 -0
  93. package/dist/context/skills-integration.test.js.map +1 -0
  94. package/dist/context/skills.d.ts +18 -0
  95. package/dist/context/skills.d.ts.map +1 -0
  96. package/dist/context/skills.js +431 -0
  97. package/dist/context/skills.js.map +1 -0
  98. package/dist/context/skills.test.d.ts +2 -0
  99. package/dist/context/skills.test.d.ts.map +1 -0
  100. package/dist/context/skills.test.js +20 -0
  101. package/dist/context/skills.test.js.map +1 -0
  102. package/dist/context/system-prompt.d.ts +2 -0
  103. package/dist/context/system-prompt.d.ts.map +1 -0
  104. package/dist/context/system-prompt.js +100 -0
  105. package/dist/context/system-prompt.js.map +1 -0
  106. package/dist/context/translation-integration.test.d.ts +2 -0
  107. package/dist/context/translation-integration.test.d.ts.map +1 -0
  108. package/dist/context/translation-integration.test.js +138 -0
  109. package/dist/context/translation-integration.test.js.map +1 -0
  110. package/dist/context/translation.d.ts +21 -0
  111. package/dist/context/translation.d.ts.map +1 -0
  112. package/dist/context/translation.js +82 -0
  113. package/dist/context/translation.js.map +1 -0
  114. package/dist/context/translation.test.d.ts +2 -0
  115. package/dist/context/translation.test.d.ts.map +1 -0
  116. package/dist/context/translation.test.js +129 -0
  117. package/dist/context/translation.test.js.map +1 -0
  118. package/dist/entrypoints/cli-input-rendering.test.d.ts +2 -0
  119. package/dist/entrypoints/cli-input-rendering.test.d.ts.map +1 -0
  120. package/dist/entrypoints/cli-input-rendering.test.js +192 -0
  121. package/dist/entrypoints/cli-input-rendering.test.js.map +1 -0
  122. package/dist/entrypoints/cli.d.ts +3 -0
  123. package/dist/entrypoints/cli.d.ts.map +1 -0
  124. package/dist/entrypoints/cli.js +1268 -0
  125. package/dist/entrypoints/cli.js.map +1 -0
  126. package/dist/entrypoints/headless-agent-config.d.ts +21 -0
  127. package/dist/entrypoints/headless-agent-config.d.ts.map +1 -0
  128. package/dist/entrypoints/headless-agent-config.js +15 -0
  129. package/dist/entrypoints/headless-agent-config.js.map +1 -0
  130. package/dist/entrypoints/headless-agent-config.test.d.ts +2 -0
  131. package/dist/entrypoints/headless-agent-config.test.d.ts.map +1 -0
  132. package/dist/entrypoints/headless-agent-config.test.js +63 -0
  133. package/dist/entrypoints/headless-agent-config.test.js.map +1 -0
  134. package/dist/entrypoints/headless.d.ts +3 -0
  135. package/dist/entrypoints/headless.d.ts.map +1 -0
  136. package/dist/entrypoints/headless.js +396 -0
  137. package/dist/entrypoints/headless.js.map +1 -0
  138. package/dist/env.d.ts +8 -0
  139. package/dist/env.d.ts.map +1 -0
  140. package/dist/env.js +14 -0
  141. package/dist/env.js.map +1 -0
  142. package/dist/friendli-models.d.ts +21 -0
  143. package/dist/friendli-models.d.ts.map +1 -0
  144. package/dist/friendli-models.js +57 -0
  145. package/dist/friendli-models.js.map +1 -0
  146. package/dist/friendli-reasoning.d.ts +10 -0
  147. package/dist/friendli-reasoning.d.ts.map +1 -0
  148. package/dist/friendli-reasoning.js +181 -0
  149. package/dist/friendli-reasoning.js.map +1 -0
  150. package/dist/friendli-reasoning.test.d.ts +2 -0
  151. package/dist/friendli-reasoning.test.d.ts.map +1 -0
  152. package/dist/friendli-reasoning.test.js +77 -0
  153. package/dist/friendli-reasoning.test.js.map +1 -0
  154. package/dist/index.d.ts +3 -0
  155. package/dist/index.d.ts.map +1 -0
  156. package/dist/index.js +3 -0
  157. package/dist/index.js.map +1 -0
  158. package/dist/interaction/colors.d.ts +22 -0
  159. package/dist/interaction/colors.d.ts.map +1 -0
  160. package/dist/interaction/colors.js +24 -0
  161. package/dist/interaction/colors.js.map +1 -0
  162. package/dist/interaction/pi-tui-stream-renderer.d.ts +19 -0
  163. package/dist/interaction/pi-tui-stream-renderer.d.ts.map +1 -0
  164. package/dist/interaction/pi-tui-stream-renderer.js +1509 -0
  165. package/dist/interaction/pi-tui-stream-renderer.js.map +1 -0
  166. package/dist/interaction/pi-tui-stream-renderer.test.d.ts +2 -0
  167. package/dist/interaction/pi-tui-stream-renderer.test.d.ts.map +1 -0
  168. package/dist/interaction/pi-tui-stream-renderer.test.js +1314 -0
  169. package/dist/interaction/pi-tui-stream-renderer.test.js.map +1 -0
  170. package/dist/interaction/spinner.d.ts +13 -0
  171. package/dist/interaction/spinner.d.ts.map +1 -0
  172. package/dist/interaction/spinner.js +51 -0
  173. package/dist/interaction/spinner.js.map +1 -0
  174. package/dist/middleware/index.d.ts +7 -0
  175. package/dist/middleware/index.d.ts.map +1 -0
  176. package/dist/middleware/index.js +15 -0
  177. package/dist/middleware/index.js.map +1 -0
  178. package/dist/middleware/todo-continuation.d.ts +11 -0
  179. package/dist/middleware/todo-continuation.d.ts.map +1 -0
  180. package/dist/middleware/todo-continuation.js +103 -0
  181. package/dist/middleware/todo-continuation.js.map +1 -0
  182. package/dist/middleware/trim-leading-newlines.d.ts +3 -0
  183. package/dist/middleware/trim-leading-newlines.d.ts.map +1 -0
  184. package/dist/middleware/trim-leading-newlines.js +49 -0
  185. package/dist/middleware/trim-leading-newlines.js.map +1 -0
  186. package/dist/reasoning-mode.d.ts +5 -0
  187. package/dist/reasoning-mode.d.ts.map +1 -0
  188. package/dist/reasoning-mode.js +30 -0
  189. package/dist/reasoning-mode.js.map +1 -0
  190. package/dist/reasoning-mode.test.d.ts +2 -0
  191. package/dist/reasoning-mode.test.d.ts.map +1 -0
  192. package/dist/reasoning-mode.test.js +22 -0
  193. package/dist/reasoning-mode.test.js.map +1 -0
  194. package/dist/tool-fallback-mode.d.ts +6 -0
  195. package/dist/tool-fallback-mode.d.ts.map +1 -0
  196. package/dist/tool-fallback-mode.js +25 -0
  197. package/dist/tool-fallback-mode.js.map +1 -0
  198. package/dist/tools/execute/shell-execute.d.ts +17 -0
  199. package/dist/tools/execute/shell-execute.d.ts.map +1 -0
  200. package/dist/tools/execute/shell-execute.js +55 -0
  201. package/dist/tools/execute/shell-execute.js.map +1 -0
  202. package/dist/tools/execute/shell-execute.test.d.ts +2 -0
  203. package/dist/tools/execute/shell-execute.test.d.ts.map +1 -0
  204. package/dist/tools/execute/shell-execute.test.js +86 -0
  205. package/dist/tools/execute/shell-execute.test.js.map +1 -0
  206. package/dist/tools/execute/shell-interact.d.ts +10 -0
  207. package/dist/tools/execute/shell-interact.d.ts.map +1 -0
  208. package/dist/tools/execute/shell-interact.js +122 -0
  209. package/dist/tools/execute/shell-interact.js.map +1 -0
  210. package/dist/tools/execute/shell-interact.test.d.ts +2 -0
  211. package/dist/tools/execute/shell-interact.test.d.ts.map +1 -0
  212. package/dist/tools/execute/shell-interact.test.js +175 -0
  213. package/dist/tools/execute/shell-interact.test.js.map +1 -0
  214. package/dist/tools/explore/glob.d.ts +15 -0
  215. package/dist/tools/explore/glob.d.ts.map +1 -0
  216. package/dist/tools/explore/glob.js +107 -0
  217. package/dist/tools/explore/glob.js.map +1 -0
  218. package/dist/tools/explore/glob.test.d.ts +2 -0
  219. package/dist/tools/explore/glob.test.d.ts.map +1 -0
  220. package/dist/tools/explore/glob.test.js +183 -0
  221. package/dist/tools/explore/glob.test.js.map +1 -0
  222. package/dist/tools/explore/grep.d.ts +27 -0
  223. package/dist/tools/explore/grep.d.ts.map +1 -0
  224. package/dist/tools/explore/grep.js +203 -0
  225. package/dist/tools/explore/grep.js.map +1 -0
  226. package/dist/tools/explore/grep.test.d.ts +2 -0
  227. package/dist/tools/explore/grep.test.d.ts.map +1 -0
  228. package/dist/tools/explore/grep.test.js +132 -0
  229. package/dist/tools/explore/grep.test.js.map +1 -0
  230. package/dist/tools/explore/read-file.d.ts +23 -0
  231. package/dist/tools/explore/read-file.d.ts.map +1 -0
  232. package/dist/tools/explore/read-file.js +84 -0
  233. package/dist/tools/explore/read-file.js.map +1 -0
  234. package/dist/tools/explore/read-file.test.d.ts +2 -0
  235. package/dist/tools/explore/read-file.test.d.ts.map +1 -0
  236. package/dist/tools/explore/read-file.test.js +278 -0
  237. package/dist/tools/explore/read-file.test.js.map +1 -0
  238. package/dist/tools/index.d.ts +71 -0
  239. package/dist/tools/index.d.ts.map +1 -0
  240. package/dist/tools/index.js +26 -0
  241. package/dist/tools/index.js.map +1 -0
  242. package/dist/tools/modify/delete-file.d.ts +19 -0
  243. package/dist/tools/modify/delete-file.d.ts.map +1 -0
  244. package/dist/tools/modify/delete-file.js +71 -0
  245. package/dist/tools/modify/delete-file.js.map +1 -0
  246. package/dist/tools/modify/delete-file.test.d.ts +2 -0
  247. package/dist/tools/modify/delete-file.test.d.ts.map +1 -0
  248. package/dist/tools/modify/delete-file.test.js +136 -0
  249. package/dist/tools/modify/delete-file.test.js.map +1 -0
  250. package/dist/tools/modify/edit-file-diagnostics.d.ts +17 -0
  251. package/dist/tools/modify/edit-file-diagnostics.d.ts.map +1 -0
  252. package/dist/tools/modify/edit-file-diagnostics.js +157 -0
  253. package/dist/tools/modify/edit-file-diagnostics.js.map +1 -0
  254. package/dist/tools/modify/edit-file-repair.d.ts +13 -0
  255. package/dist/tools/modify/edit-file-repair.d.ts.map +1 -0
  256. package/dist/tools/modify/edit-file-repair.js +135 -0
  257. package/dist/tools/modify/edit-file-repair.js.map +1 -0
  258. package/dist/tools/modify/edit-file-stress.test.d.ts +2 -0
  259. package/dist/tools/modify/edit-file-stress.test.d.ts.map +1 -0
  260. package/dist/tools/modify/edit-file-stress.test.js +163 -0
  261. package/dist/tools/modify/edit-file-stress.test.js.map +1 -0
  262. package/dist/tools/modify/edit-file-validation.d.ts +9 -0
  263. package/dist/tools/modify/edit-file-validation.d.ts.map +1 -0
  264. package/dist/tools/modify/edit-file-validation.js +86 -0
  265. package/dist/tools/modify/edit-file-validation.js.map +1 -0
  266. package/dist/tools/modify/edit-file-whitespace.test.d.ts +2 -0
  267. package/dist/tools/modify/edit-file-whitespace.test.d.ts.map +1 -0
  268. package/dist/tools/modify/edit-file-whitespace.test.js +90 -0
  269. package/dist/tools/modify/edit-file-whitespace.test.js.map +1 -0
  270. package/dist/tools/modify/edit-file.d.ts +33 -0
  271. package/dist/tools/modify/edit-file.d.ts.map +1 -0
  272. package/dist/tools/modify/edit-file.js +177 -0
  273. package/dist/tools/modify/edit-file.js.map +1 -0
  274. package/dist/tools/modify/edit-file.test.d.ts +2 -0
  275. package/dist/tools/modify/edit-file.test.d.ts.map +1 -0
  276. package/dist/tools/modify/edit-file.test.js +948 -0
  277. package/dist/tools/modify/edit-file.test.js.map +1 -0
  278. package/dist/tools/modify/write-file.d.ts +17 -0
  279. package/dist/tools/modify/write-file.d.ts.map +1 -0
  280. package/dist/tools/modify/write-file.js +39 -0
  281. package/dist/tools/modify/write-file.js.map +1 -0
  282. package/dist/tools/modify/write-file.test.d.ts +2 -0
  283. package/dist/tools/modify/write-file.test.d.ts.map +1 -0
  284. package/dist/tools/modify/write-file.test.js +168 -0
  285. package/dist/tools/modify/write-file.test.js.map +1 -0
  286. package/dist/tools/planning/load-skill.d.ts +13 -0
  287. package/dist/tools/planning/load-skill.d.ts.map +1 -0
  288. package/dist/tools/planning/load-skill.js +101 -0
  289. package/dist/tools/planning/load-skill.js.map +1 -0
  290. package/dist/tools/planning/load-skill.test.d.ts +2 -0
  291. package/dist/tools/planning/load-skill.test.d.ts.map +1 -0
  292. package/dist/tools/planning/load-skill.test.js +37 -0
  293. package/dist/tools/planning/load-skill.test.js.map +1 -0
  294. package/dist/tools/planning/todo-write.d.ts +49 -0
  295. package/dist/tools/planning/todo-write.d.ts.map +1 -0
  296. package/dist/tools/planning/todo-write.js +118 -0
  297. package/dist/tools/planning/todo-write.js.map +1 -0
  298. package/dist/tools/planning/todo-write.test.d.ts +2 -0
  299. package/dist/tools/planning/todo-write.test.d.ts.map +1 -0
  300. package/dist/tools/planning/todo-write.test.js +82 -0
  301. package/dist/tools/planning/todo-write.test.js.map +1 -0
  302. package/dist/tools/utils/execute/format-utils.d.ts +4 -0
  303. package/dist/tools/utils/execute/format-utils.d.ts.map +1 -0
  304. package/dist/tools/utils/execute/format-utils.js +31 -0
  305. package/dist/tools/utils/execute/format-utils.js.map +1 -0
  306. package/dist/tools/utils/execute/format-utils.test.d.ts +2 -0
  307. package/dist/tools/utils/execute/format-utils.test.d.ts.map +1 -0
  308. package/dist/tools/utils/execute/format-utils.test.js +40 -0
  309. package/dist/tools/utils/execute/format-utils.test.js.map +1 -0
  310. package/dist/tools/utils/execute/noninteractive-wrapper.d.ts +12 -0
  311. package/dist/tools/utils/execute/noninteractive-wrapper.d.ts.map +1 -0
  312. package/dist/tools/utils/execute/noninteractive-wrapper.js +269 -0
  313. package/dist/tools/utils/execute/noninteractive-wrapper.js.map +1 -0
  314. package/dist/tools/utils/execute/noninteractive-wrapper.test.d.ts +2 -0
  315. package/dist/tools/utils/execute/noninteractive-wrapper.test.d.ts.map +1 -0
  316. package/dist/tools/utils/execute/noninteractive-wrapper.test.js +233 -0
  317. package/dist/tools/utils/execute/noninteractive-wrapper.test.js.map +1 -0
  318. package/dist/tools/utils/execute/output-handler.d.ts +14 -0
  319. package/dist/tools/utils/execute/output-handler.d.ts.map +1 -0
  320. package/dist/tools/utils/execute/output-handler.js +71 -0
  321. package/dist/tools/utils/execute/output-handler.js.map +1 -0
  322. package/dist/tools/utils/execute/output-handler.test.d.ts +2 -0
  323. package/dist/tools/utils/execute/output-handler.test.d.ts.map +1 -0
  324. package/dist/tools/utils/execute/output-handler.test.js +58 -0
  325. package/dist/tools/utils/execute/output-handler.test.js.map +1 -0
  326. package/dist/tools/utils/execute/process-manager.d.ts +17 -0
  327. package/dist/tools/utils/execute/process-manager.d.ts.map +1 -0
  328. package/dist/tools/utils/execute/process-manager.js +229 -0
  329. package/dist/tools/utils/execute/process-manager.js.map +1 -0
  330. package/dist/tools/utils/execute/process-manager.test.d.ts +2 -0
  331. package/dist/tools/utils/execute/process-manager.test.d.ts.map +1 -0
  332. package/dist/tools/utils/execute/process-manager.test.js +139 -0
  333. package/dist/tools/utils/execute/process-manager.test.js.map +1 -0
  334. package/dist/tools/utils/execute/shell-detection.d.ts +3 -0
  335. package/dist/tools/utils/execute/shell-detection.d.ts.map +1 -0
  336. package/dist/tools/utils/execute/shell-detection.js +56 -0
  337. package/dist/tools/utils/execute/shell-detection.js.map +1 -0
  338. package/dist/tools/utils/execute/shell-detection.test.d.ts +2 -0
  339. package/dist/tools/utils/execute/shell-detection.test.d.ts.map +1 -0
  340. package/dist/tools/utils/execute/shell-detection.test.js +86 -0
  341. package/dist/tools/utils/execute/shell-detection.test.js.map +1 -0
  342. package/dist/tools/utils/hashline/autocorrect-replacement-lines.d.ts +5 -0
  343. package/dist/tools/utils/hashline/autocorrect-replacement-lines.d.ts.map +1 -0
  344. package/dist/tools/utils/hashline/autocorrect-replacement-lines.js +113 -0
  345. package/dist/tools/utils/hashline/autocorrect-replacement-lines.js.map +1 -0
  346. package/dist/tools/utils/hashline/constants.d.ts +5 -0
  347. package/dist/tools/utils/hashline/constants.d.ts.map +1 -0
  348. package/dist/tools/utils/hashline/constants.js +11 -0
  349. package/dist/tools/utils/hashline/constants.js.map +1 -0
  350. package/dist/tools/utils/hashline/diff-utils.d.ts +7 -0
  351. package/dist/tools/utils/hashline/diff-utils.d.ts.map +1 -0
  352. package/dist/tools/utils/hashline/diff-utils.js +50 -0
  353. package/dist/tools/utils/hashline/diff-utils.js.map +1 -0
  354. package/dist/tools/utils/hashline/diff-utils.test.d.ts +2 -0
  355. package/dist/tools/utils/hashline/diff-utils.test.d.ts.map +1 -0
  356. package/dist/tools/utils/hashline/diff-utils.test.js +46 -0
  357. package/dist/tools/utils/hashline/diff-utils.test.js.map +1 -0
  358. package/dist/tools/utils/hashline/edit-deduplication.d.ts +6 -0
  359. package/dist/tools/utils/hashline/edit-deduplication.d.ts.map +1 -0
  360. package/dist/tools/utils/hashline/edit-deduplication.js +32 -0
  361. package/dist/tools/utils/hashline/edit-deduplication.js.map +1 -0
  362. package/dist/tools/utils/hashline/edit-operation-primitives.d.ts +11 -0
  363. package/dist/tools/utils/hashline/edit-operation-primitives.d.ts.map +1 -0
  364. package/dist/tools/utils/hashline/edit-operation-primitives.js +93 -0
  365. package/dist/tools/utils/hashline/edit-operation-primitives.js.map +1 -0
  366. package/dist/tools/utils/hashline/edit-operations.d.ts +9 -0
  367. package/dist/tools/utils/hashline/edit-operations.d.ts.map +1 -0
  368. package/dist/tools/utils/hashline/edit-operations.js +101 -0
  369. package/dist/tools/utils/hashline/edit-operations.js.map +1 -0
  370. package/dist/tools/utils/hashline/edit-operations.test.d.ts +2 -0
  371. package/dist/tools/utils/hashline/edit-operations.test.d.ts.map +1 -0
  372. package/dist/tools/utils/hashline/edit-operations.test.js +135 -0
  373. package/dist/tools/utils/hashline/edit-operations.test.js.map +1 -0
  374. package/dist/tools/utils/hashline/edit-ordering.d.ts +5 -0
  375. package/dist/tools/utils/hashline/edit-ordering.d.ts.map +1 -0
  376. package/dist/tools/utils/hashline/edit-ordering.js +54 -0
  377. package/dist/tools/utils/hashline/edit-ordering.js.map +1 -0
  378. package/dist/tools/utils/hashline/edit-text-normalization.d.ts +9 -0
  379. package/dist/tools/utils/hashline/edit-text-normalization.d.ts.map +1 -0
  380. package/dist/tools/utils/hashline/edit-text-normalization.js +135 -0
  381. package/dist/tools/utils/hashline/edit-text-normalization.js.map +1 -0
  382. package/dist/tools/utils/hashline/file-text-canonicalization.d.ts +8 -0
  383. package/dist/tools/utils/hashline/file-text-canonicalization.d.ts.map +1 -0
  384. package/dist/tools/utils/hashline/file-text-canonicalization.js +42 -0
  385. package/dist/tools/utils/hashline/file-text-canonicalization.js.map +1 -0
  386. package/dist/tools/utils/hashline/hash-computation.d.ts +14 -0
  387. package/dist/tools/utils/hashline/hash-computation.d.ts.map +1 -0
  388. package/dist/tools/utils/hashline/hash-computation.js +158 -0
  389. package/dist/tools/utils/hashline/hash-computation.js.map +1 -0
  390. package/dist/tools/utils/hashline/hash-computation.test.d.ts +2 -0
  391. package/dist/tools/utils/hashline/hash-computation.test.d.ts.map +1 -0
  392. package/dist/tools/utils/hashline/hash-computation.test.js +63 -0
  393. package/dist/tools/utils/hashline/hash-computation.test.js.map +1 -0
  394. package/dist/tools/utils/hashline/hashline-chunk-formatter.d.ts +11 -0
  395. package/dist/tools/utils/hashline/hashline-chunk-formatter.d.ts.map +1 -0
  396. package/dist/tools/utils/hashline/hashline-chunk-formatter.js +41 -0
  397. package/dist/tools/utils/hashline/hashline-chunk-formatter.js.map +1 -0
  398. package/dist/tools/utils/hashline/hashline-edit-diff.d.ts +2 -0
  399. package/dist/tools/utils/hashline/hashline-edit-diff.d.ts.map +1 -0
  400. package/dist/tools/utils/hashline/hashline-edit-diff.js +27 -0
  401. package/dist/tools/utils/hashline/hashline-edit-diff.js.map +1 -0
  402. package/dist/tools/utils/hashline/index.d.ts +14 -0
  403. package/dist/tools/utils/hashline/index.d.ts.map +1 -0
  404. package/dist/tools/utils/hashline/index.js +11 -0
  405. package/dist/tools/utils/hashline/index.js.map +1 -0
  406. package/dist/tools/utils/hashline/merge-expansion.d.ts +4 -0
  407. package/dist/tools/utils/hashline/merge-expansion.d.ts.map +1 -0
  408. package/dist/tools/utils/hashline/merge-expansion.js +105 -0
  409. package/dist/tools/utils/hashline/merge-expansion.js.map +1 -0
  410. package/dist/tools/utils/hashline/normalize-edits.d.ts +11 -0
  411. package/dist/tools/utils/hashline/normalize-edits.d.ts.map +1 -0
  412. package/dist/tools/utils/hashline/normalize-edits.js +81 -0
  413. package/dist/tools/utils/hashline/normalize-edits.js.map +1 -0
  414. package/dist/tools/utils/hashline/types.d.ts +18 -0
  415. package/dist/tools/utils/hashline/types.d.ts.map +1 -0
  416. package/dist/tools/utils/hashline/types.js +2 -0
  417. package/dist/tools/utils/hashline/types.js.map +1 -0
  418. package/dist/tools/utils/hashline/validation.d.ts +19 -0
  419. package/dist/tools/utils/hashline/validation.d.ts.map +1 -0
  420. package/dist/tools/utils/hashline/validation.js +161 -0
  421. package/dist/tools/utils/hashline/validation.js.map +1 -0
  422. package/dist/tools/utils/hashline/validation.test.d.ts +2 -0
  423. package/dist/tools/utils/hashline/validation.test.d.ts.map +1 -0
  424. package/dist/tools/utils/hashline/validation.test.js +86 -0
  425. package/dist/tools/utils/hashline/validation.test.js.map +1 -0
  426. package/dist/tools/utils/safety-utils.d.ts +66 -0
  427. package/dist/tools/utils/safety-utils.d.ts.map +1 -0
  428. package/dist/tools/utils/safety-utils.js +681 -0
  429. package/dist/tools/utils/safety-utils.js.map +1 -0
  430. package/dist/utils/tools-manager.d.ts +16 -0
  431. package/dist/utils/tools-manager.d.ts.map +1 -0
  432. package/dist/utils/tools-manager.js +257 -0
  433. package/dist/utils/tools-manager.js.map +1 -0
  434. package/package.json +49 -0
  435. package/src/AGENTS.md +52 -0
  436. package/src/agent-reasoning-default.test.ts +48 -0
  437. package/src/agent.test.ts +49 -0
  438. package/src/agent.ts +455 -0
  439. package/src/cli-args.test.ts +152 -0
  440. package/src/cli-args.ts +90 -0
  441. package/src/commands/aliases-and-tool-fallback.test.ts +172 -0
  442. package/src/commands/clear.ts +14 -0
  443. package/src/commands/factories/create-toggle-command.ts +68 -0
  444. package/src/commands/help.ts +30 -0
  445. package/src/commands/index.ts +125 -0
  446. package/src/commands/model.ts +146 -0
  447. package/src/commands/reasoning-mode.ts +55 -0
  448. package/src/commands/render.test.ts +47 -0
  449. package/src/commands/render.ts +205 -0
  450. package/src/commands/tool-fallback.ts +47 -0
  451. package/src/commands/translate.test.ts +62 -0
  452. package/src/commands/translate.ts +14 -0
  453. package/src/commands/types.ts +18 -0
  454. package/src/context/environment-context.ts +11 -0
  455. package/src/context/paths.ts +2 -0
  456. package/src/context/session.ts +18 -0
  457. package/src/context/skill-command-prefix.ts +18 -0
  458. package/src/context/skills-integration.test.ts +113 -0
  459. package/src/context/skills.test.ts +25 -0
  460. package/src/context/skills.ts +566 -0
  461. package/src/context/system-prompt.ts +100 -0
  462. package/src/context/translation-integration.test.ts +194 -0
  463. package/src/context/translation.test.ts +186 -0
  464. package/src/context/translation.ts +122 -0
  465. package/src/entrypoints/AGENTS.md +33 -0
  466. package/src/entrypoints/cli-input-rendering.test.ts +236 -0
  467. package/src/entrypoints/cli.ts +1845 -0
  468. package/src/entrypoints/headless-agent-config.test.ts +82 -0
  469. package/src/entrypoints/headless-agent-config.ts +42 -0
  470. package/src/entrypoints/headless.ts +622 -0
  471. package/src/env.ts +14 -0
  472. package/src/friendli-models.ts +81 -0
  473. package/src/friendli-reasoning.test.ts +147 -0
  474. package/src/friendli-reasoning.ts +280 -0
  475. package/src/index.ts +3 -0
  476. package/src/interaction/colors.ts +24 -0
  477. package/src/interaction/pi-tui-stream-renderer.test.ts +1471 -0
  478. package/src/interaction/pi-tui-stream-renderer.ts +2150 -0
  479. package/src/interaction/spinner.ts +61 -0
  480. package/src/middleware/index.ts +32 -0
  481. package/src/middleware/todo-continuation.ts +128 -0
  482. package/src/middleware/trim-leading-newlines.ts +66 -0
  483. package/src/reasoning-mode.test.ts +24 -0
  484. package/src/reasoning-mode.ts +40 -0
  485. package/src/skills/example/SKILL.md +44 -0
  486. package/src/skills/example/references/api.md +37 -0
  487. package/src/skills/example/scripts/setup.sh +13 -0
  488. package/src/skills/git-workflow.md +405 -0
  489. package/src/tool-fallback-mode.ts +34 -0
  490. package/src/tools/AGENTS.md +44 -0
  491. package/src/tools/execute/shell-execute.test.ts +114 -0
  492. package/src/tools/execute/shell-execute.ts +74 -0
  493. package/src/tools/execute/shell-execute.txt +27 -0
  494. package/src/tools/execute/shell-interact.test.ts +236 -0
  495. package/src/tools/execute/shell-interact.ts +151 -0
  496. package/src/tools/execute/shell-interact.txt +15 -0
  497. package/src/tools/explore/glob-files.txt +8 -0
  498. package/src/tools/explore/glob.test.ts +217 -0
  499. package/src/tools/explore/glob.ts +137 -0
  500. package/src/tools/explore/grep-files.txt +12 -0
  501. package/src/tools/explore/grep.test.ts +183 -0
  502. package/src/tools/explore/grep.ts +266 -0
  503. package/src/tools/explore/read-file.test.ts +355 -0
  504. package/src/tools/explore/read-file.ts +102 -0
  505. package/src/tools/explore/read-file.txt +24 -0
  506. package/src/tools/index.ts +29 -0
  507. package/src/tools/modify/AGENTS.md +38 -0
  508. package/src/tools/modify/delete-file.test.ts +200 -0
  509. package/src/tools/modify/delete-file.ts +95 -0
  510. package/src/tools/modify/delete-file.txt +9 -0
  511. package/src/tools/modify/edit-file-diagnostics.ts +210 -0
  512. package/src/tools/modify/edit-file-repair.ts +183 -0
  513. package/src/tools/modify/edit-file-stress.test.ts +200 -0
  514. package/src/tools/modify/edit-file-validation.ts +134 -0
  515. package/src/tools/modify/edit-file-whitespace.test.ts +117 -0
  516. package/src/tools/modify/edit-file.test.ts +1231 -0
  517. package/src/tools/modify/edit-file.ts +252 -0
  518. package/src/tools/modify/edit-file.txt +73 -0
  519. package/src/tools/modify/write-file.test.ts +240 -0
  520. package/src/tools/modify/write-file.ts +56 -0
  521. package/src/tools/modify/write-file.txt +9 -0
  522. package/src/tools/planning/load-skill.test.ts +48 -0
  523. package/src/tools/planning/load-skill.ts +136 -0
  524. package/src/tools/planning/load-skill.txt +6 -0
  525. package/src/tools/planning/todo-write.test.ts +91 -0
  526. package/src/tools/planning/todo-write.ts +141 -0
  527. package/src/tools/planning/todo-write.txt +7 -0
  528. package/src/tools/utils/execute/format-utils.test.ts +53 -0
  529. package/src/tools/utils/execute/format-utils.ts +37 -0
  530. package/src/tools/utils/execute/noninteractive-wrapper.test.ts +306 -0
  531. package/src/tools/utils/execute/noninteractive-wrapper.ts +314 -0
  532. package/src/tools/utils/execute/output-handler.test.ts +72 -0
  533. package/src/tools/utils/execute/output-handler.ts +101 -0
  534. package/src/tools/utils/execute/process-manager.test.ts +175 -0
  535. package/src/tools/utils/execute/process-manager.ts +310 -0
  536. package/src/tools/utils/execute/shell-detection.test.ts +112 -0
  537. package/src/tools/utils/execute/shell-detection.ts +72 -0
  538. package/src/tools/utils/hashline/autocorrect-replacement-lines.ts +159 -0
  539. package/src/tools/utils/hashline/constants.ts +13 -0
  540. package/src/tools/utils/hashline/diff-utils.test.ts +61 -0
  541. package/src/tools/utils/hashline/diff-utils.ts +64 -0
  542. package/src/tools/utils/hashline/edit-deduplication.ts +40 -0
  543. package/src/tools/utils/hashline/edit-operation-primitives.ts +149 -0
  544. package/src/tools/utils/hashline/edit-operations.test.ts +154 -0
  545. package/src/tools/utils/hashline/edit-operations.ts +132 -0
  546. package/src/tools/utils/hashline/edit-ordering.ts +60 -0
  547. package/src/tools/utils/hashline/edit-text-normalization.ts +180 -0
  548. package/src/tools/utils/hashline/file-text-canonicalization.ts +58 -0
  549. package/src/tools/utils/hashline/hash-computation.test.ts +82 -0
  550. package/src/tools/utils/hashline/hash-computation.ts +199 -0
  551. package/src/tools/utils/hashline/hashline-chunk-formatter.ts +61 -0
  552. package/src/tools/utils/hashline/hashline-edit-diff.ts +35 -0
  553. package/src/tools/utils/hashline/index.ts +55 -0
  554. package/src/tools/utils/hashline/merge-expansion.ts +120 -0
  555. package/src/tools/utils/hashline/normalize-edits.ts +127 -0
  556. package/src/tools/utils/hashline/types.ts +20 -0
  557. package/src/tools/utils/hashline/validation.test.ts +109 -0
  558. package/src/tools/utils/hashline/validation.ts +212 -0
  559. package/src/tools/utils/safety-utils.ts +938 -0
  560. package/src/utils/tools-manager.ts +353 -0
@@ -0,0 +1,2150 @@
1
+ import {
2
+ Container,
3
+ Markdown,
4
+ type MarkdownTheme,
5
+ Spacer,
6
+ Text,
7
+ truncateToWidth,
8
+ visibleWidth,
9
+ } from "@mariozechner/pi-tui";
10
+ import { parsePartialJson, type TextStreamPart, type ToolSet } from "ai";
11
+
12
+ type StreamPart = TextStreamPart<ToolSet>;
13
+
14
+ interface ToolInputRenderState {
15
+ hasContent: boolean;
16
+ inputBuffer: string;
17
+ renderedInputLength: number;
18
+ toolName: string;
19
+ }
20
+
21
+ export interface PiTuiStreamRenderOptions {
22
+ chatContainer: Container;
23
+ markdownTheme: MarkdownTheme;
24
+ onFirstVisiblePart?: () => void;
25
+ showFiles?: boolean;
26
+ showFinishReason?: boolean;
27
+ showRawToolIo?: boolean;
28
+ showReasoning?: boolean;
29
+ showSources?: boolean;
30
+ showSteps?: boolean;
31
+ showToolResults?: boolean;
32
+ ui: {
33
+ requestRender: () => void;
34
+ };
35
+ }
36
+
37
+ interface ToolInputPart {
38
+ id?: string;
39
+ toolCallId?: string;
40
+ }
41
+
42
+ interface ToolInputDeltaPart extends ToolInputPart {
43
+ delta?: unknown;
44
+ inputTextDelta?: unknown;
45
+ }
46
+
47
+ const addChatComponent = (
48
+ chatContainer: Container,
49
+ component: Container | Text | Markdown,
50
+ options: { addLeadingSpacer?: boolean } = {}
51
+ ): void => {
52
+ if (options.addLeadingSpacer ?? true) {
53
+ chatContainer.addChild(new Spacer(1));
54
+ }
55
+ chatContainer.addChild(component);
56
+ };
57
+
58
+ const safeStringify = (value: unknown): string => {
59
+ if (typeof value === "string") {
60
+ return value;
61
+ }
62
+ try {
63
+ return JSON.stringify(value, null, 2);
64
+ } catch {
65
+ return String(value);
66
+ }
67
+ };
68
+
69
+ const isPlainEmptyObject = (value: unknown): boolean => {
70
+ if (typeof value !== "object" || value === null) {
71
+ return false;
72
+ }
73
+
74
+ if (Array.isArray(value)) {
75
+ return false;
76
+ }
77
+
78
+ return Object.keys(value).length === 0;
79
+ };
80
+
81
+ const renderCodeBlock = (language: string, value: unknown): string => {
82
+ const text = safeStringify(value).replace(TRAILING_NEWLINES, "");
83
+ const longestFenceRun = Array.from(
84
+ text.matchAll(BACKTICK_FENCE_PATTERN)
85
+ ).reduce((max, match) => Math.max(max, match[0].length), 2);
86
+ const fence = "`".repeat(longestFenceRun + 1);
87
+ return `${fence}${language}\n${text}\n${fence}`;
88
+ };
89
+
90
+ const READ_FILE_SUCCESS_PREFIX = "OK - read file";
91
+ const GLOB_SUCCESS_PREFIX = "OK - glob";
92
+ const GREP_SUCCESS_PREFIX = "OK - grep";
93
+ const SHELL_EXECUTE_TOOL_NAMES = new Set(["shell_execute", "bash"]);
94
+ const SHELL_TOOL_NAMES = new Set(["shell_execute", "bash", "shell_interact"]);
95
+ const UNKNOWN_TOOL_NAME = "tool";
96
+ const READ_FILE_BLOCK_PREFIX = "======== ";
97
+ const READ_FILE_BLOCK_SUFFIX = " ========";
98
+ const READ_FILE_BLOCK_END = "======== end ========";
99
+ const BACKTICK_FENCE_PATTERN = /`{3,}/g;
100
+ const READ_FILE_LINE_SPLIT_PATTERN = /^(\s*\d+(?:#[^\s|]+)?\s*\|\s*)(.*)$/;
101
+ const READ_FILE_LINES_WITH_RETURNED_PATTERN = /^(\d+)\s+\(returned:\s*(\d+)\)$/;
102
+ const READ_FILE_MARKDOWN_FENCE_PATTERN = /^(?:`{3,}|~{3,}).*$/;
103
+ const SURROUNDED_BY_DOUBLE_QUOTES_PATTERN = /^"(.*)"$/;
104
+ const TAB_PATTERN = /\t/g;
105
+ const MAX_READ_PREVIEW_LINES = 10;
106
+ const MAX_WRITE_FILE_PREVIEW_LINES = 200;
107
+ const MAX_WRITE_FILE_PREVIEW_CHARS = 100_000;
108
+ const TOOL_PENDING_MESSAGE = "Executing..";
109
+ const TOOL_PENDING_MARKER = "__tool_pending_status__";
110
+ const TOOL_PENDING_SPINNER_FRAMES = ["-", "\\", "|", "/"] as const;
111
+ const HASHLINE_TAG_ONLY_PATTERN = /^(.*\d+#[ZPMQVRWSNKTXJBYH]{2})\s*$/;
112
+ const HASHLINE_PIPE_ONLY_PATTERN = /^\|\s*(.*)$/;
113
+ const HASHLINE_TAG_PIPE_ONLY_PATTERN =
114
+ /^(.*\d+#[ZPMQVRWSNKTXJBYH]{2})\s*\|\s*$/;
115
+ const HASHLINE_COMPACT_LINE_PATTERN = /^\s*\d+#[ZPMQVRWSNKTXJBYH]{2}\|.*$/;
116
+
117
+ const isTruthyEnvFlag = (value: string | undefined): boolean => {
118
+ if (!value) {
119
+ return false;
120
+ }
121
+
122
+ const normalized = value.trim().toLowerCase();
123
+ return (
124
+ normalized === "1" ||
125
+ normalized === "true" ||
126
+ normalized === "yes" ||
127
+ normalized === "on"
128
+ );
129
+ };
130
+
131
+ const isRawToolIoEnabledByEnv = (): boolean => {
132
+ return isTruthyEnvFlag(process.env.DEBUG_SHOW_RAW_TOOL_IO);
133
+ };
134
+
135
+ interface ReadFileParsedOutput {
136
+ blockBody: string;
137
+ blockTitle: string;
138
+ metadata: Map<string, string>;
139
+ }
140
+
141
+ const extractStringField = (input: unknown, field: string): string | null => {
142
+ if (typeof input !== "object" || input === null) {
143
+ return null;
144
+ }
145
+ const record = input as Record<string, unknown>;
146
+ return typeof record[field] === "string" ? (record[field] as string) : null;
147
+ };
148
+
149
+ const extractNumberField = (input: unknown, field: string): number | null => {
150
+ if (typeof input !== "object" || input === null) {
151
+ return null;
152
+ }
153
+ const record = input as Record<string, unknown>;
154
+ const value = record[field];
155
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
156
+ };
157
+
158
+ const extractBooleanField = (input: unknown, field: string): boolean | null => {
159
+ if (typeof input !== "object" || input === null) {
160
+ return null;
161
+ }
162
+ const record = input as Record<string, unknown>;
163
+ return typeof record[field] === "boolean" ? (record[field] as boolean) : null;
164
+ };
165
+
166
+ const buildTextPreviewLines = (
167
+ text: string,
168
+ emptyText = "(empty)",
169
+ maxPreviewLines = MAX_READ_PREVIEW_LINES
170
+ ): string[] => {
171
+ const normalized = text.replaceAll("\r\n", "\n").trimEnd();
172
+ if (normalized.length === 0) {
173
+ return [emptyText];
174
+ }
175
+
176
+ const allLines = normalized.split("\n");
177
+ const omittedLines = Math.max(0, allLines.length - maxPreviewLines);
178
+ const visibleLines =
179
+ omittedLines > 0 ? allLines.slice(0, maxPreviewLines) : allLines;
180
+
181
+ const preview = [...visibleLines];
182
+ if (omittedLines > 0) {
183
+ const lineLabel = `line${omittedLines === 1 ? "" : "s"}`;
184
+ preview.push("");
185
+ preview.push(`... (${omittedLines} more ${lineLabel})`);
186
+ }
187
+
188
+ return preview;
189
+ };
190
+
191
+ const buildPrettyHeader = (title: string, target: string): string => {
192
+ return `**${title}** \`${target}\``;
193
+ };
194
+
195
+ const normalizeHashlineBreakArtifacts = (lines: string[]): string[] => {
196
+ const normalized: string[] = [];
197
+
198
+ for (let index = 0; index < lines.length; index += 1) {
199
+ const current = lines[index].replaceAll("\r", "");
200
+ const next = lines[index + 1]?.replaceAll("\r", "");
201
+ const nextNext = lines[index + 2]?.replaceAll("\r", "");
202
+
203
+ const currentTagOnly = current.match(HASHLINE_TAG_ONLY_PATTERN);
204
+ const nextPipeOnly = next?.match(HASHLINE_PIPE_ONLY_PATTERN);
205
+ const nextLooksLikeHashline =
206
+ next !== undefined && HASHLINE_COMPACT_LINE_PATTERN.test(next);
207
+ const nextNextLooksLikeHashline =
208
+ nextNext !== undefined && HASHLINE_COMPACT_LINE_PATTERN.test(nextNext);
209
+
210
+ if (currentTagOnly && nextPipeOnly) {
211
+ const pipedContent = nextPipeOnly[1].trim();
212
+ if (pipedContent.length > 0) {
213
+ normalized.push(`${currentTagOnly[1]}|${pipedContent}`);
214
+ index += 1;
215
+ continue;
216
+ }
217
+
218
+ if (nextNext !== undefined && !nextNextLooksLikeHashline) {
219
+ normalized.push(`${currentTagOnly[1]}|${nextNext.trimStart()}`);
220
+ index += 2;
221
+ continue;
222
+ }
223
+ }
224
+
225
+ const currentTagPipeOnly = current.match(HASHLINE_TAG_PIPE_ONLY_PATTERN);
226
+ if (currentTagPipeOnly && next !== undefined && !nextLooksLikeHashline) {
227
+ normalized.push(`${currentTagPipeOnly[1]}|${next.trimStart()}`);
228
+ index += 1;
229
+ continue;
230
+ }
231
+
232
+ normalized.push(current);
233
+ }
234
+
235
+ return normalized;
236
+ };
237
+
238
+ const shouldIncludeReadFilePreviewLine = (line: string): boolean => {
239
+ const match = line.match(READ_FILE_LINE_SPLIT_PATTERN);
240
+ const content = (match?.[2] ?? line).trim();
241
+ return !READ_FILE_MARKDOWN_FENCE_PATTERN.test(content);
242
+ };
243
+
244
+ interface ReadFileRenderPayload {
245
+ body: string;
246
+ path: string;
247
+ range: string | null;
248
+ }
249
+
250
+ interface GlobRenderPayload {
251
+ body: string;
252
+ pattern: string;
253
+ }
254
+
255
+ interface GrepRenderPayload {
256
+ body: string;
257
+ pattern: string;
258
+ }
259
+
260
+ interface GlobPreviewMetadata {
261
+ truncated: boolean;
262
+ }
263
+
264
+ interface GrepPreviewMetadata {
265
+ matchCount: string | null;
266
+ path: string | null;
267
+ truncated: boolean;
268
+ }
269
+
270
+ const stripSurroundedDoubleQuotes = (value: string): string => {
271
+ const trimmed = value.trim();
272
+ const matched = trimmed.match(SURROUNDED_BY_DOUBLE_QUOTES_PATTERN);
273
+ return matched?.[1] ?? trimmed;
274
+ };
275
+
276
+ const parseReadFileMetadataLine = (
277
+ line: string
278
+ ): { key: string; value: string } | null => {
279
+ const separatorIndex = line.indexOf(":");
280
+ if (separatorIndex <= 0) {
281
+ return null;
282
+ }
283
+
284
+ const key = line.slice(0, separatorIndex).trim();
285
+ if (key.length === 0) {
286
+ return null;
287
+ }
288
+
289
+ return {
290
+ key,
291
+ value: line.slice(separatorIndex + 1).trim(),
292
+ };
293
+ };
294
+
295
+ const parseNumberedBlockToolOutput = (
296
+ output: string,
297
+ successPrefix: string
298
+ ): ReadFileParsedOutput | null => {
299
+ const normalized = output.replaceAll("\r\n", "\n");
300
+ if (!normalized.startsWith(successPrefix)) {
301
+ return null;
302
+ }
303
+
304
+ const lines = normalized.split("\n");
305
+ const metadata = new Map<string, string>();
306
+ let blockStartIndex = -1;
307
+
308
+ for (let index = 1; index < lines.length; index += 1) {
309
+ const line = lines[index];
310
+
311
+ if (
312
+ line.startsWith(READ_FILE_BLOCK_PREFIX) &&
313
+ line.endsWith(READ_FILE_BLOCK_SUFFIX) &&
314
+ line !== READ_FILE_BLOCK_END
315
+ ) {
316
+ blockStartIndex = index;
317
+ break;
318
+ }
319
+
320
+ if (line.trim().length === 0) {
321
+ continue;
322
+ }
323
+
324
+ const parsed = parseReadFileMetadataLine(line);
325
+ if (!parsed) {
326
+ return null;
327
+ }
328
+ metadata.set(parsed.key, parsed.value);
329
+ }
330
+
331
+ if (blockStartIndex < 0) {
332
+ return null;
333
+ }
334
+
335
+ const blockEndIndex = lines.findIndex(
336
+ (line, index) => index > blockStartIndex && line === READ_FILE_BLOCK_END
337
+ );
338
+ if (blockEndIndex < 0) {
339
+ return null;
340
+ }
341
+
342
+ const blockTitle = lines[blockStartIndex]
343
+ .slice(READ_FILE_BLOCK_PREFIX.length, -READ_FILE_BLOCK_SUFFIX.length)
344
+ .trim();
345
+ const blockBody = lines.slice(blockStartIndex + 1, blockEndIndex).join("\n");
346
+
347
+ return {
348
+ metadata,
349
+ blockTitle,
350
+ blockBody,
351
+ };
352
+ };
353
+
354
+ const parseReadFileOutput = (output: string): ReadFileParsedOutput | null => {
355
+ return parseNumberedBlockToolOutput(output, READ_FILE_SUCCESS_PREFIX);
356
+ };
357
+
358
+ const parseGlobOutput = (output: string): ReadFileParsedOutput | null => {
359
+ return parseNumberedBlockToolOutput(output, GLOB_SUCCESS_PREFIX);
360
+ };
361
+
362
+ const parseGrepOutput = (output: string): ReadFileParsedOutput | null => {
363
+ return parseNumberedBlockToolOutput(output, GREP_SUCCESS_PREFIX);
364
+ };
365
+
366
+ const resolveReadPath = (parsed: ReadFileParsedOutput): string => {
367
+ const pathValue = parsed.metadata.get("path") ?? "";
368
+ return pathValue.trim() || parsed.blockTitle || "(unknown)";
369
+ };
370
+
371
+ const getToolOmittedLineCount = (metadata: Map<string, string>): number => {
372
+ const linesMetadata = metadata.get("lines");
373
+ if (!linesMetadata) {
374
+ return 0;
375
+ }
376
+
377
+ const matchedCounts = linesMetadata.match(
378
+ READ_FILE_LINES_WITH_RETURNED_PATTERN
379
+ );
380
+ if (!matchedCounts) {
381
+ return 0;
382
+ }
383
+
384
+ const totalLines = Number.parseInt(matchedCounts[1], 10);
385
+ const returnedLines = Number.parseInt(matchedCounts[2], 10);
386
+ if (!(Number.isFinite(totalLines) && Number.isFinite(returnedLines))) {
387
+ return 0;
388
+ }
389
+
390
+ const isTruncated = metadata.get("truncated")?.toLowerCase() === "true";
391
+ if (!isTruncated) {
392
+ return 0;
393
+ }
394
+
395
+ return Math.max(0, totalLines - returnedLines);
396
+ };
397
+
398
+ const buildReadPreviewLines = (
399
+ visibleLines: string[],
400
+ totalOmitted: number,
401
+ isModelTruncated: boolean
402
+ ): string[] => {
403
+ const previewLines =
404
+ visibleLines.length > 0 && visibleLines.some((line) => line.length > 0)
405
+ ? [...visibleLines]
406
+ : ["(empty)"];
407
+
408
+ if (totalOmitted <= 0) {
409
+ return previewLines;
410
+ }
411
+
412
+ const lineLabel = `line${totalOmitted === 1 ? "" : "s"}`;
413
+ previewLines.push("");
414
+ previewLines.push(
415
+ isModelTruncated
416
+ ? `... (${totalOmitted} more ${lineLabel}, truncated)`
417
+ : `... (${totalOmitted} more ${lineLabel})`
418
+ );
419
+
420
+ return previewLines;
421
+ };
422
+
423
+ const parseIntegerMetadataValue = (
424
+ rawValue: string | undefined
425
+ ): number | null => {
426
+ if (!rawValue) {
427
+ return null;
428
+ }
429
+
430
+ const parsed = Number.parseInt(rawValue, 10);
431
+ return Number.isFinite(parsed) ? parsed : null;
432
+ };
433
+
434
+ const buildGlobPreviewLines = (
435
+ visibleLines: string[],
436
+ totalOmitted: number,
437
+ metadata: GlobPreviewMetadata
438
+ ): string[] => {
439
+ const previewLines =
440
+ visibleLines.length > 0 && visibleLines.some((line) => line.length > 0)
441
+ ? [...visibleLines]
442
+ : ["(no matches)"];
443
+
444
+ if (totalOmitted <= 0) {
445
+ return previewLines;
446
+ }
447
+
448
+ const lineLabel = `line${totalOmitted === 1 ? "" : "s"}`;
449
+
450
+ previewLines.push("");
451
+ previewLines.push(
452
+ metadata.truncated
453
+ ? `... (${totalOmitted} more ${lineLabel}, truncated)`
454
+ : `... (${totalOmitted} more ${lineLabel})`
455
+ );
456
+
457
+ return previewLines;
458
+ };
459
+
460
+ const buildGrepPreviewLines = (
461
+ visibleLines: string[],
462
+ totalOmitted: number,
463
+ metadata: GrepPreviewMetadata
464
+ ): string[] => {
465
+ const previewLines =
466
+ visibleLines.length > 0 && visibleLines.some((line) => line.length > 0)
467
+ ? [...visibleLines]
468
+ : ["(no matches)"];
469
+
470
+ if (totalOmitted <= 0) {
471
+ return previewLines;
472
+ }
473
+
474
+ const lineLabel = `line${totalOmitted === 1 ? "" : "s"}`;
475
+ const metadataParts = [`path: ${metadata.path ?? "."}`];
476
+ if (metadata.matchCount) {
477
+ metadataParts.push(`match_count (${metadata.matchCount})`);
478
+ }
479
+ if (metadata.truncated) {
480
+ metadataParts.push("truncated: true");
481
+ }
482
+
483
+ previewLines.push("");
484
+ previewLines.push(
485
+ metadata.truncated
486
+ ? `... (${totalOmitted} more ${lineLabel}, truncated)`
487
+ : `... (${totalOmitted} more ${lineLabel})`
488
+ );
489
+ previewLines.push(metadataParts.join(", "));
490
+
491
+ return previewLines;
492
+ };
493
+
494
+ const renderReadFileOutput = (output: string): ReadFileRenderPayload | null => {
495
+ const parsed = parseReadFileOutput(output);
496
+ if (!parsed) {
497
+ return null;
498
+ }
499
+
500
+ const readPath = resolveReadPath(parsed);
501
+
502
+ const contentBody =
503
+ parsed.blockBody.trim().length > 0 ? parsed.blockBody : "(empty)";
504
+ const allLines = normalizeHashlineBreakArtifacts(
505
+ contentBody.split("\n").filter(shouldIncludeReadFilePreviewLine)
506
+ );
507
+ const omittedFromPreview = Math.max(
508
+ 0,
509
+ allLines.length - MAX_READ_PREVIEW_LINES
510
+ );
511
+ const visibleLines =
512
+ omittedFromPreview > 0
513
+ ? allLines.slice(0, MAX_READ_PREVIEW_LINES)
514
+ : allLines;
515
+
516
+ const omittedFromTool = getToolOmittedLineCount(parsed.metadata);
517
+ const isModelTruncated =
518
+ parsed.metadata.get("truncated")?.toLowerCase() === "true";
519
+ const totalOmitted = omittedFromPreview + omittedFromTool;
520
+ const previewLines = buildReadPreviewLines(
521
+ visibleLines,
522
+ totalOmitted,
523
+ isModelTruncated
524
+ );
525
+ const rangeValue = parsed.metadata.get("range")?.trim() || null;
526
+
527
+ return {
528
+ path: readPath,
529
+ range: rangeValue,
530
+ body: previewLines.join("\n"),
531
+ };
532
+ };
533
+
534
+ const renderGlobOutput = (output: string): GlobRenderPayload | null => {
535
+ const parsed = parseGlobOutput(output);
536
+ if (!parsed) {
537
+ return null;
538
+ }
539
+
540
+ const contentBody = parsed.blockBody.trim();
541
+ const allLines =
542
+ contentBody.length > 0
543
+ ? contentBody.split("\n").filter((line) => line.trim().length > 0)
544
+ : [];
545
+
546
+ const omittedFromPreview = Math.max(
547
+ 0,
548
+ allLines.length - MAX_READ_PREVIEW_LINES
549
+ );
550
+ const visibleLines =
551
+ omittedFromPreview > 0
552
+ ? allLines.slice(0, MAX_READ_PREVIEW_LINES)
553
+ : allLines;
554
+
555
+ const fileCountRaw = parsed.metadata.get("file_count");
556
+ const fileCount = parseIntegerMetadataValue(fileCountRaw);
557
+ const isToolTruncated =
558
+ parsed.metadata.get("truncated")?.toLowerCase() === "true";
559
+ const omittedFromTool =
560
+ isToolTruncated && fileCount !== null
561
+ ? Math.max(0, fileCount - allLines.length)
562
+ : 0;
563
+ const totalOmitted = omittedFromPreview + omittedFromTool;
564
+
565
+ const metadata: GlobPreviewMetadata = {
566
+ truncated: isToolTruncated,
567
+ };
568
+
569
+ const previewLines = buildGlobPreviewLines(
570
+ visibleLines,
571
+ totalOmitted,
572
+ metadata
573
+ );
574
+ const patternValue =
575
+ stripSurroundedDoubleQuotes(parsed.metadata.get("pattern") ?? "") ||
576
+ parsed.blockTitle ||
577
+ "(unknown)";
578
+
579
+ return {
580
+ pattern: patternValue,
581
+ body: previewLines.join("\n"),
582
+ };
583
+ };
584
+
585
+ const renderGrepOutput = (output: string): GrepRenderPayload | null => {
586
+ const parsed = parseGrepOutput(output);
587
+ if (!parsed) {
588
+ return null;
589
+ }
590
+
591
+ const contentBody = parsed.blockBody.trim();
592
+ const allLines =
593
+ contentBody.length > 0
594
+ ? normalizeHashlineBreakArtifacts(contentBody.split("\n"))
595
+ : [];
596
+
597
+ const omittedFromPreview = Math.max(
598
+ 0,
599
+ allLines.length - MAX_READ_PREVIEW_LINES
600
+ );
601
+ const visibleLines =
602
+ omittedFromPreview > 0
603
+ ? allLines.slice(0, MAX_READ_PREVIEW_LINES)
604
+ : allLines;
605
+
606
+ const matchCountRaw = parsed.metadata.get("match_count");
607
+ const matchCount = parseIntegerMetadataValue(matchCountRaw);
608
+ const isToolTruncated =
609
+ parsed.metadata.get("truncated")?.toLowerCase() === "true";
610
+ const omittedFromTool =
611
+ isToolTruncated && matchCount !== null
612
+ ? Math.max(0, matchCount - allLines.length)
613
+ : 0;
614
+ const totalOmitted = omittedFromPreview + omittedFromTool;
615
+
616
+ const metadata: GrepPreviewMetadata = {
617
+ path: parsed.metadata.get("path") ?? null,
618
+ matchCount: matchCountRaw ?? null,
619
+ truncated: isToolTruncated,
620
+ };
621
+
622
+ const previewLines = buildGrepPreviewLines(
623
+ visibleLines,
624
+ totalOmitted,
625
+ metadata
626
+ );
627
+
628
+ const patternValue =
629
+ stripSurroundedDoubleQuotes(parsed.metadata.get("pattern") ?? "") ||
630
+ parsed.blockTitle ||
631
+ "(unknown)";
632
+
633
+ return {
634
+ pattern: patternValue,
635
+ body: previewLines.join("\n"),
636
+ };
637
+ };
638
+
639
+ const renderPendingOutput = (): string => {
640
+ return TOOL_PENDING_MARKER;
641
+ };
642
+
643
+ const buildPendingSpinnerText = (frame: string): string => {
644
+ return `${frame} ${TOOL_PENDING_MESSAGE}`;
645
+ };
646
+
647
+ const renderToolOutput = (_toolName: string, output: unknown): string => {
648
+ return renderCodeBlock("text", output);
649
+ };
650
+
651
+ const ANSI_RESET = "\x1b[0m";
652
+ const ANSI_DIM = "\x1b[2m";
653
+ const ANSI_ITALIC = "\x1b[3m";
654
+ const ANSI_GRAY = "\x1b[90m";
655
+ const ANSI_BG_GRAY = "\x1b[100m";
656
+ const LEADING_NEWLINES = /^\n+/;
657
+ const TRAILING_NEWLINES = /\n+$/;
658
+
659
+ const applyReadPreviewBackground = (text: string): string => {
660
+ return `${ANSI_BG_GRAY}${text}${ANSI_RESET}`;
661
+ };
662
+
663
+ const styleThinkingText = (text: string): string => {
664
+ return `${ANSI_DIM}${ANSI_ITALIC}${ANSI_GRAY}${text}${ANSI_RESET}`;
665
+ };
666
+
667
+ class TrimmedMarkdown extends Markdown {
668
+ override render(width: number): string[] {
669
+ const lines = super.render(width);
670
+ let end = lines.length;
671
+ while (end > 0 && lines[end - 1].trim().length === 0) {
672
+ end -= 1;
673
+ }
674
+ return lines.slice(0, end);
675
+ }
676
+ }
677
+
678
+ class TruncatedReadBody {
679
+ private cachedLines?: string[];
680
+ private cachedText?: string;
681
+ private cachedWidth?: number;
682
+ private readonly background?: (text: string) => string;
683
+ private backgroundEnabled = true;
684
+ private readonly paddingX: number;
685
+ private text: string;
686
+
687
+ constructor(
688
+ text: string,
689
+ paddingX: number,
690
+ background?: (text: string) => string
691
+ ) {
692
+ this.text = text;
693
+ this.paddingX = paddingX;
694
+ this.background = background;
695
+ }
696
+
697
+ setText(text: string): void {
698
+ this.text = text;
699
+ this.cachedText = undefined;
700
+ this.cachedWidth = undefined;
701
+ this.cachedLines = undefined;
702
+ }
703
+
704
+ setBackgroundEnabled(enabled: boolean): void {
705
+ if (this.backgroundEnabled === enabled) {
706
+ return;
707
+ }
708
+ this.backgroundEnabled = enabled;
709
+ this.invalidate();
710
+ }
711
+
712
+ invalidate(): void {
713
+ this.cachedText = undefined;
714
+ this.cachedWidth = undefined;
715
+ this.cachedLines = undefined;
716
+ }
717
+
718
+ render(width: number): string[] {
719
+ if (
720
+ this.cachedLines &&
721
+ this.cachedText === this.text &&
722
+ this.cachedWidth === width
723
+ ) {
724
+ return this.cachedLines;
725
+ }
726
+
727
+ if (!this.text || this.text.trim().length === 0) {
728
+ this.cachedText = this.text;
729
+ this.cachedWidth = width;
730
+ this.cachedLines = [];
731
+ return [];
732
+ }
733
+
734
+ const normalizedText = this.text.replace(TAB_PATTERN, " ");
735
+ const contentWidth = Math.max(1, width - this.paddingX * 2);
736
+ const leftMargin = " ".repeat(this.paddingX);
737
+ const rightMargin = " ".repeat(this.paddingX);
738
+
739
+ const renderedLines = normalizedText.split("\n").map((line) => {
740
+ const truncatedLine = truncateToWidth(line, contentWidth, "");
741
+ const lineWithMargins = `${leftMargin}${truncatedLine}${rightMargin}`;
742
+ const visibleLength = visibleWidth(lineWithMargins);
743
+ const paddedLine = `${lineWithMargins}${" ".repeat(Math.max(0, width - visibleLength))}`;
744
+
745
+ return this.background && this.backgroundEnabled
746
+ ? this.background(paddedLine)
747
+ : paddedLine;
748
+ });
749
+
750
+ this.cachedText = this.text;
751
+ this.cachedWidth = width;
752
+ this.cachedLines = renderedLines;
753
+ return renderedLines;
754
+ }
755
+ }
756
+
757
+ interface DiffLine {
758
+ text: string;
759
+ type: "add" | "context" | "delete";
760
+ }
761
+
762
+ const MAX_DIFF_MATRIX_CELLS = 60_000;
763
+ const MAX_DIFF_RENDER_LINES = 160;
764
+
765
+ const buildSimpleDiff = (before: string, after: string): DiffLine[] => {
766
+ const beforeLines = before.split("\n");
767
+ const afterLines = after.split("\n");
768
+ const lines: DiffLine[] = [];
769
+ const maxLength = Math.max(beforeLines.length, afterLines.length);
770
+
771
+ for (let index = 0; index < maxLength; index += 1) {
772
+ const oldLine = beforeLines[index];
773
+ const newLine = afterLines[index];
774
+
775
+ if (oldLine === undefined && newLine !== undefined) {
776
+ lines.push({ type: "add", text: newLine });
777
+ continue;
778
+ }
779
+
780
+ if (newLine === undefined && oldLine !== undefined) {
781
+ lines.push({ type: "delete", text: oldLine });
782
+ continue;
783
+ }
784
+
785
+ if (oldLine === newLine && oldLine !== undefined) {
786
+ lines.push({ type: "context", text: oldLine });
787
+ continue;
788
+ }
789
+
790
+ if (oldLine !== undefined) {
791
+ lines.push({ type: "delete", text: oldLine });
792
+ }
793
+ if (newLine !== undefined) {
794
+ lines.push({ type: "add", text: newLine });
795
+ }
796
+ }
797
+
798
+ return lines;
799
+ };
800
+
801
+ const buildLcsDiff = (before: string, after: string): DiffLine[] => {
802
+ const beforeLines = before.split("\n");
803
+ const afterLines = after.split("\n");
804
+
805
+ if (beforeLines.length === 0 && afterLines.length === 0) {
806
+ return [];
807
+ }
808
+
809
+ const matrixCells = (beforeLines.length + 1) * (afterLines.length + 1);
810
+ if (matrixCells > MAX_DIFF_MATRIX_CELLS) {
811
+ return buildSimpleDiff(before, after);
812
+ }
813
+
814
+ const lcs: number[][] = new Array(beforeLines.length + 1)
815
+ .fill(undefined)
816
+ .map(() => new Array(afterLines.length + 1).fill(0));
817
+
818
+ for (let i = beforeLines.length - 1; i >= 0; i -= 1) {
819
+ for (let j = afterLines.length - 1; j >= 0; j -= 1) {
820
+ if (beforeLines[i] === afterLines[j]) {
821
+ lcs[i][j] = lcs[i + 1][j + 1] + 1;
822
+ } else {
823
+ lcs[i][j] = Math.max(lcs[i + 1][j], lcs[i][j + 1]);
824
+ }
825
+ }
826
+ }
827
+
828
+ const lines: DiffLine[] = [];
829
+ let i = 0;
830
+ let j = 0;
831
+
832
+ while (i < beforeLines.length && j < afterLines.length) {
833
+ if (beforeLines[i] === afterLines[j]) {
834
+ lines.push({ type: "context", text: beforeLines[i] });
835
+ i += 1;
836
+ j += 1;
837
+ continue;
838
+ }
839
+
840
+ if (lcs[i + 1][j] >= lcs[i][j + 1]) {
841
+ lines.push({ type: "delete", text: beforeLines[i] });
842
+ i += 1;
843
+ continue;
844
+ }
845
+
846
+ lines.push({ type: "add", text: afterLines[j] });
847
+ j += 1;
848
+ }
849
+
850
+ while (i < beforeLines.length) {
851
+ lines.push({ type: "delete", text: beforeLines[i] });
852
+ i += 1;
853
+ }
854
+
855
+ while (j < afterLines.length) {
856
+ lines.push({ type: "add", text: afterLines[j] });
857
+ j += 1;
858
+ }
859
+
860
+ return lines;
861
+ };
862
+
863
+ const renderDiffBlock = (before: string, after: string): string => {
864
+ const diffLines = buildLcsDiff(before, after);
865
+
866
+ const rendered: string[] = ["```diff"];
867
+ for (let index = 0; index < diffLines.length; index += 1) {
868
+ if (index >= MAX_DIFF_RENDER_LINES) {
869
+ rendered.push("... diff truncated ...");
870
+ break;
871
+ }
872
+
873
+ const line = diffLines[index];
874
+ if (line.type === "add") {
875
+ rendered.push(`+${line.text}`);
876
+ } else if (line.type === "delete") {
877
+ rendered.push(`-${line.text}`);
878
+ } else {
879
+ rendered.push(` ${line.text}`);
880
+ }
881
+ }
882
+ rendered.push("```");
883
+
884
+ return rendered.join("\n");
885
+ };
886
+
887
+ const tryExtractEditPayload = (
888
+ toolName: string,
889
+ input: unknown
890
+ ): { newStr: string; oldStr: string; path?: string } | null => {
891
+ if (toolName !== "edit_file") {
892
+ return null;
893
+ }
894
+
895
+ if (typeof input !== "object" || input === null) {
896
+ return null;
897
+ }
898
+
899
+ const record = input as Record<string, unknown>;
900
+ const oldStr = record.old_str;
901
+ const newStr = record.new_str;
902
+ const path = record.path;
903
+
904
+ if (typeof oldStr !== "string" || typeof newStr !== "string") {
905
+ return null;
906
+ }
907
+
908
+ return {
909
+ oldStr,
910
+ newStr,
911
+ path: typeof path === "string" ? path : undefined,
912
+ };
913
+ };
914
+
915
+ class AssistantStreamView extends Container {
916
+ private readonly markdownTheme: MarkdownTheme;
917
+ private readonly segments: Array<{
918
+ content: string;
919
+ type: "reasoning" | "text";
920
+ }> = [];
921
+
922
+ constructor(markdownTheme: MarkdownTheme) {
923
+ super();
924
+ this.markdownTheme = markdownTheme;
925
+ this.refresh();
926
+ }
927
+
928
+ appendReasoning(delta: string): void {
929
+ this.appendSegment("reasoning", delta);
930
+ }
931
+
932
+ appendText(delta: string): void {
933
+ this.appendSegment("text", delta);
934
+ }
935
+
936
+ private appendSegment(type: "reasoning" | "text", delta: string): void {
937
+ if (delta.length === 0) {
938
+ return;
939
+ }
940
+
941
+ const lastSegment = this.segments.at(-1);
942
+ if (lastSegment && lastSegment.type === type) {
943
+ lastSegment.content += delta;
944
+ } else {
945
+ this.segments.push({
946
+ type,
947
+ content: delta,
948
+ });
949
+ }
950
+
951
+ this.refresh();
952
+ }
953
+
954
+ private refresh(): void {
955
+ this.clear();
956
+
957
+ const visibleSegments = this.segments
958
+ .map((segment) => {
959
+ const normalizedContent =
960
+ segment.type === "reasoning"
961
+ ? segment.content.replace(LEADING_NEWLINES, "").trimEnd()
962
+ : segment.content.trim();
963
+
964
+ return {
965
+ ...segment,
966
+ content: normalizedContent,
967
+ };
968
+ })
969
+ .filter((segment) => segment.content.trim().length > 0);
970
+
971
+ if (visibleSegments.length === 0) {
972
+ return;
973
+ }
974
+
975
+ for (let index = 0; index < visibleSegments.length; index += 1) {
976
+ const segment = visibleSegments[index];
977
+ const text = segment.content;
978
+
979
+ if (segment.type === "text") {
980
+ this.addChild(new Markdown(text, 1, 0, this.markdownTheme));
981
+ } else {
982
+ this.addChild(
983
+ new Markdown(text, 1, 0, this.markdownTheme, {
984
+ color: styleThinkingText,
985
+ italic: true,
986
+ })
987
+ );
988
+ }
989
+
990
+ if (index < visibleSegments.length - 1) {
991
+ this.addChild(new Spacer(1));
992
+ }
993
+ }
994
+ }
995
+ }
996
+
997
+ class ToolCallView extends Container {
998
+ private readonly callId: string;
999
+ private readonly content: TrimmedMarkdown;
1000
+ private readonly readBlock: Container;
1001
+ private readonly readBody: TruncatedReadBody;
1002
+ private readonly readHeader: TrimmedMarkdown;
1003
+ private readonly requestRender: () => void;
1004
+ private readonly showRawToolIo: boolean;
1005
+ private error: unknown;
1006
+ private finalInput: unknown;
1007
+ private inputBuffer = "";
1008
+ private output: unknown;
1009
+ private outputDenied = false;
1010
+ private pendingSpinnerFrameIndex = 0;
1011
+ private pendingSpinnerInterval: Timer | null = null;
1012
+ private pendingTemplate: string | null = null;
1013
+ private parsedInput: unknown;
1014
+ private readMode = false;
1015
+ private toolName: string;
1016
+
1017
+ constructor(
1018
+ callId: string,
1019
+ toolName: string,
1020
+ markdownTheme: MarkdownTheme,
1021
+ requestRender: () => void,
1022
+ showRawToolIo: boolean
1023
+ ) {
1024
+ super();
1025
+ this.callId = callId;
1026
+ this.toolName = toolName;
1027
+ this.requestRender = requestRender;
1028
+ this.showRawToolIo = showRawToolIo;
1029
+ this.content = new TrimmedMarkdown("", 1, 0, markdownTheme);
1030
+ this.readHeader = new TrimmedMarkdown("", 1, 0, markdownTheme);
1031
+ this.readBody = new TruncatedReadBody("", 1, applyReadPreviewBackground);
1032
+ this.readBlock = new Container();
1033
+ this.readBlock.addChild(this.readHeader);
1034
+ this.readBlock.addChild(new Spacer(1));
1035
+ this.readBlock.addChild(this.readBody);
1036
+ this.addChild(this.content);
1037
+ this.refresh();
1038
+ }
1039
+
1040
+ dispose(): void {
1041
+ this.stopPendingSpinner();
1042
+ }
1043
+
1044
+ private setReadMode(enabled: boolean): void {
1045
+ if (this.readMode === enabled) {
1046
+ return;
1047
+ }
1048
+
1049
+ this.readMode = enabled;
1050
+ this.clear();
1051
+ this.addChild(enabled ? this.readBlock : this.content);
1052
+ }
1053
+
1054
+ private stopPendingSpinner(): void {
1055
+ this.pendingTemplate = null;
1056
+ if (!this.pendingSpinnerInterval) {
1057
+ return;
1058
+ }
1059
+ clearInterval(this.pendingSpinnerInterval);
1060
+ this.pendingSpinnerInterval = null;
1061
+ }
1062
+
1063
+ private applyPendingSpinnerFrame(): void {
1064
+ if (!this.pendingTemplate) {
1065
+ return;
1066
+ }
1067
+
1068
+ const frame = TOOL_PENDING_SPINNER_FRAMES[this.pendingSpinnerFrameIndex];
1069
+ this.readBody.setText(
1070
+ this.pendingTemplate.replaceAll(
1071
+ TOOL_PENDING_MARKER,
1072
+ buildPendingSpinnerText(frame)
1073
+ )
1074
+ );
1075
+ }
1076
+
1077
+ private startPendingSpinner(template: string): void {
1078
+ this.pendingTemplate = template;
1079
+ this.pendingSpinnerFrameIndex = 0;
1080
+ this.applyPendingSpinnerFrame();
1081
+
1082
+ if (this.pendingSpinnerInterval) {
1083
+ return;
1084
+ }
1085
+
1086
+ this.pendingSpinnerInterval = setInterval(() => {
1087
+ this.pendingSpinnerFrameIndex =
1088
+ (this.pendingSpinnerFrameIndex + 1) %
1089
+ TOOL_PENDING_SPINNER_FRAMES.length;
1090
+ this.applyPendingSpinnerFrame();
1091
+ this.requestRender();
1092
+ }, 80);
1093
+ }
1094
+
1095
+ async appendInputChunk(chunk: string): Promise<void> {
1096
+ this.inputBuffer += chunk;
1097
+
1098
+ const { value, state } = await parsePartialJson(this.inputBuffer);
1099
+ const shouldSuppressTransientEmptyObject =
1100
+ state !== "successful-parse" && isPlainEmptyObject(value);
1101
+
1102
+ if (!shouldSuppressTransientEmptyObject) {
1103
+ this.parsedInput = value;
1104
+ }
1105
+
1106
+ this.refresh();
1107
+ }
1108
+
1109
+ setError(error: unknown): void {
1110
+ this.error = error;
1111
+ this.refresh();
1112
+ }
1113
+
1114
+ setFinalInput(input: unknown): void {
1115
+ this.finalInput = input;
1116
+ this.refresh();
1117
+ }
1118
+
1119
+ setOutput(output: unknown): void {
1120
+ this.output = output;
1121
+ this.refresh();
1122
+ }
1123
+
1124
+ setOutputDenied(): void {
1125
+ this.outputDenied = true;
1126
+ this.refresh();
1127
+ }
1128
+
1129
+ setToolName(toolName: string): void {
1130
+ this.toolName = toolName;
1131
+ this.refresh();
1132
+ }
1133
+
1134
+ private resolveBestInput(): unknown {
1135
+ if (this.finalInput !== undefined) {
1136
+ return this.finalInput;
1137
+ }
1138
+ if (this.parsedInput !== undefined) {
1139
+ return this.parsedInput;
1140
+ }
1141
+ if (this.inputBuffer.length > 0) {
1142
+ return this.inputBuffer;
1143
+ }
1144
+ return undefined;
1145
+ }
1146
+
1147
+ private canRenderPrettyTool(toolName: string | Set<string>): boolean {
1148
+ if (this.error !== undefined || this.outputDenied) {
1149
+ return false;
1150
+ }
1151
+
1152
+ if (typeof toolName === "string") {
1153
+ return this.toolName === toolName;
1154
+ }
1155
+
1156
+ return toolName.has(this.toolName);
1157
+ }
1158
+
1159
+ private resolveInputStringField(field: string): string | null {
1160
+ const bestInput = this.resolveBestInput();
1161
+ const fromObject = extractStringField(bestInput, field);
1162
+ if (fromObject) {
1163
+ return fromObject;
1164
+ }
1165
+
1166
+ return null;
1167
+ }
1168
+
1169
+ private resolveInputNumberField(field: string): number | null {
1170
+ const bestInput = this.resolveBestInput();
1171
+ const fromObject = extractNumberField(bestInput, field);
1172
+ if (fromObject !== null) {
1173
+ return fromObject;
1174
+ }
1175
+
1176
+ return null;
1177
+ }
1178
+
1179
+ private setPrettyBlock(
1180
+ header: string,
1181
+ body: string,
1182
+ options?: { isPending?: boolean; useBackground?: boolean }
1183
+ ): void {
1184
+ this.setReadMode(true);
1185
+ this.readBody.setBackgroundEnabled(options?.useBackground ?? true);
1186
+ this.readHeader.setText(header);
1187
+
1188
+ if (options?.isPending) {
1189
+ this.startPendingSpinner(body);
1190
+ return;
1191
+ }
1192
+
1193
+ this.stopPendingSpinner();
1194
+ this.readBody.setText(body);
1195
+ }
1196
+
1197
+ private renderOutputPreviewBody(
1198
+ output: unknown,
1199
+ emptyText = "(no output)"
1200
+ ): string {
1201
+ if (typeof output === "string") {
1202
+ return buildTextPreviewLines(output, emptyText).join("\n");
1203
+ }
1204
+
1205
+ return buildTextPreviewLines(safeStringify(output), emptyText).join("\n");
1206
+ }
1207
+
1208
+ private tryRenderReadFileMode(): boolean {
1209
+ if (
1210
+ this.toolName !== "read_file" ||
1211
+ this.error !== undefined ||
1212
+ this.outputDenied
1213
+ ) {
1214
+ return false;
1215
+ }
1216
+
1217
+ const readPath = this.resolveInputStringField("path");
1218
+
1219
+ if (typeof this.output === "string") {
1220
+ const renderedReadFile = renderReadFileOutput(this.output);
1221
+ if (renderedReadFile) {
1222
+ const pathWithRange = renderedReadFile.range
1223
+ ? `${renderedReadFile.path} ${renderedReadFile.range}`
1224
+ : renderedReadFile.path;
1225
+ this.setPrettyBlock(
1226
+ `**Read** \`${pathWithRange}\``,
1227
+ renderedReadFile.body,
1228
+ {
1229
+ useBackground: true,
1230
+ }
1231
+ );
1232
+ } else {
1233
+ const fallbackPath = readPath ?? "(unknown)";
1234
+ this.setPrettyBlock(
1235
+ `**Read** \`${fallbackPath}\``,
1236
+ safeStringify(this.output),
1237
+ {
1238
+ useBackground: true,
1239
+ }
1240
+ );
1241
+ }
1242
+ return true;
1243
+ }
1244
+
1245
+ if (!readPath) {
1246
+ return false;
1247
+ }
1248
+
1249
+ this.setPrettyBlock(`**Read** \`${readPath}\``, renderPendingOutput(), {
1250
+ useBackground: false,
1251
+ isPending: true,
1252
+ });
1253
+ return true;
1254
+ }
1255
+
1256
+ private tryRenderGlobMode(): boolean {
1257
+ if (
1258
+ this.toolName !== "glob_files" ||
1259
+ this.error !== undefined ||
1260
+ this.outputDenied
1261
+ ) {
1262
+ return false;
1263
+ }
1264
+
1265
+ const globPattern = this.resolveInputStringField("pattern");
1266
+
1267
+ if (typeof this.output === "string") {
1268
+ const renderedGlob = renderGlobOutput(this.output);
1269
+ if (renderedGlob) {
1270
+ this.setPrettyBlock(
1271
+ `**Glob** \`${renderedGlob.pattern}\``,
1272
+ renderedGlob.body,
1273
+ {
1274
+ useBackground: true,
1275
+ }
1276
+ );
1277
+ } else {
1278
+ const fallbackPattern = globPattern ?? "(unknown)";
1279
+ this.setPrettyBlock(
1280
+ `**Glob** \`${fallbackPattern}\``,
1281
+ safeStringify(this.output),
1282
+ {
1283
+ useBackground: true,
1284
+ }
1285
+ );
1286
+ }
1287
+ return true;
1288
+ }
1289
+
1290
+ if (!globPattern) {
1291
+ return false;
1292
+ }
1293
+
1294
+ this.setPrettyBlock(`**Glob** \`${globPattern}\``, renderPendingOutput(), {
1295
+ useBackground: false,
1296
+ isPending: true,
1297
+ });
1298
+ return true;
1299
+ }
1300
+
1301
+ private tryRenderGrepMode(): boolean {
1302
+ if (
1303
+ this.toolName !== "grep_files" ||
1304
+ this.error !== undefined ||
1305
+ this.outputDenied
1306
+ ) {
1307
+ return false;
1308
+ }
1309
+
1310
+ const grepPattern = this.resolveInputStringField("pattern");
1311
+
1312
+ if (typeof this.output === "string") {
1313
+ const renderedGrep = renderGrepOutput(this.output);
1314
+ if (renderedGrep) {
1315
+ this.setPrettyBlock(
1316
+ `**Grep** \`${renderedGrep.pattern}\``,
1317
+ renderedGrep.body,
1318
+ {
1319
+ useBackground: true,
1320
+ }
1321
+ );
1322
+ } else {
1323
+ const fallbackPattern = grepPattern ?? "(unknown)";
1324
+ this.setPrettyBlock(
1325
+ `**Grep** \`${fallbackPattern}\``,
1326
+ safeStringify(this.output),
1327
+ {
1328
+ useBackground: true,
1329
+ }
1330
+ );
1331
+ }
1332
+ return true;
1333
+ }
1334
+
1335
+ if (!grepPattern) {
1336
+ return false;
1337
+ }
1338
+
1339
+ this.setPrettyBlock(`**Grep** \`${grepPattern}\``, renderPendingOutput(), {
1340
+ useBackground: false,
1341
+ isPending: true,
1342
+ });
1343
+ return true;
1344
+ }
1345
+
1346
+ private tryRenderShellExecuteMode(): boolean {
1347
+ if (!this.canRenderPrettyTool(SHELL_EXECUTE_TOOL_NAMES)) {
1348
+ return false;
1349
+ }
1350
+
1351
+ const command = this.resolveInputStringField("command") ?? "(command)";
1352
+ const header = buildPrettyHeader("Shell", command);
1353
+
1354
+ if (this.output === undefined) {
1355
+ this.setPrettyBlock(header, renderPendingOutput(), {
1356
+ isPending: true,
1357
+ useBackground: false,
1358
+ });
1359
+ return true;
1360
+ }
1361
+
1362
+ const exitCode = extractNumberField(this.output, "exit_code");
1363
+ const shellOutput = extractStringField(this.output, "output");
1364
+ const workdir = this.resolveInputStringField("workdir");
1365
+ const timeoutMs = this.resolveInputNumberField("timeout_ms");
1366
+
1367
+ const bodyLines: string[] = [];
1368
+ if (exitCode !== null) {
1369
+ bodyLines.push(`exit_code: ${exitCode}`);
1370
+ }
1371
+ if (workdir) {
1372
+ bodyLines.push(`workdir: ${workdir}`);
1373
+ }
1374
+ if (timeoutMs !== null) {
1375
+ bodyLines.push(`timeout_ms: ${timeoutMs}`);
1376
+ }
1377
+ if (bodyLines.length > 0) {
1378
+ bodyLines.push("");
1379
+ }
1380
+
1381
+ bodyLines.push(
1382
+ ...buildTextPreviewLines(
1383
+ shellOutput ?? safeStringify(this.output),
1384
+ "(no output)"
1385
+ )
1386
+ );
1387
+
1388
+ this.setPrettyBlock(header, bodyLines.join("\n"));
1389
+ return true;
1390
+ }
1391
+
1392
+ private tryRenderShellInteractMode(): boolean {
1393
+ if (!this.canRenderPrettyTool("shell_interact")) {
1394
+ return false;
1395
+ }
1396
+
1397
+ const keystrokes =
1398
+ this.resolveInputStringField("keystrokes") ?? "(keystrokes)";
1399
+ const header = buildPrettyHeader("Interact", keystrokes);
1400
+
1401
+ if (this.output === undefined) {
1402
+ this.setPrettyBlock(header, renderPendingOutput(), {
1403
+ isPending: true,
1404
+ useBackground: false,
1405
+ });
1406
+ return true;
1407
+ }
1408
+
1409
+ const success = extractBooleanField(this.output, "success");
1410
+ const interactOutput = extractStringField(this.output, "output");
1411
+
1412
+ const bodyLines: string[] = [];
1413
+ if (success !== null) {
1414
+ bodyLines.push(`success: ${success}`);
1415
+ bodyLines.push("");
1416
+ }
1417
+
1418
+ bodyLines.push(
1419
+ ...buildTextPreviewLines(
1420
+ interactOutput ?? safeStringify(this.output),
1421
+ "(no output)"
1422
+ )
1423
+ );
1424
+
1425
+ this.setPrettyBlock(header, bodyLines.join("\n"));
1426
+ return true;
1427
+ }
1428
+
1429
+ private tryRenderWriteFileMode(): boolean {
1430
+ if (!this.canRenderPrettyTool("write_file")) {
1431
+ return false;
1432
+ }
1433
+
1434
+ const bestInput = this.resolveBestInput();
1435
+ const path = extractStringField(bestInput, "path") ?? "(unknown)";
1436
+ const fileContent = extractStringField(bestInput, "content");
1437
+ const header = buildPrettyHeader("Write", path);
1438
+
1439
+ const hasVisibleFileContent =
1440
+ fileContent !== null && fileContent.trim().length > 0;
1441
+
1442
+ if (hasVisibleFileContent) {
1443
+ const boundedContent =
1444
+ fileContent.length > MAX_WRITE_FILE_PREVIEW_CHARS
1445
+ ? fileContent.slice(0, MAX_WRITE_FILE_PREVIEW_CHARS)
1446
+ : fileContent;
1447
+ const previewBody = buildTextPreviewLines(
1448
+ boundedContent,
1449
+ "(empty)",
1450
+ MAX_WRITE_FILE_PREVIEW_LINES
1451
+ ).join("\n");
1452
+
1453
+ this.setPrettyBlock(header, previewBody, {
1454
+ useBackground: true,
1455
+ });
1456
+ return true;
1457
+ }
1458
+
1459
+ if (this.output === undefined) {
1460
+ this.setPrettyBlock(header, renderPendingOutput(), {
1461
+ isPending: true,
1462
+ useBackground: false,
1463
+ });
1464
+ return true;
1465
+ }
1466
+
1467
+ this.setPrettyBlock(header, this.renderOutputPreviewBody(this.output));
1468
+ return true;
1469
+ }
1470
+
1471
+ private tryRenderEditFileMode(): boolean {
1472
+ if (!this.canRenderPrettyTool("edit_file")) {
1473
+ return false;
1474
+ }
1475
+
1476
+ const bestInput = this.resolveBestInput();
1477
+ const path = this.resolveInputStringField("path") ?? "(unknown)";
1478
+ const header = buildPrettyHeader("Edit", path);
1479
+ const bodyLines: string[] = [];
1480
+
1481
+ if (typeof bestInput === "object" && bestInput !== null) {
1482
+ const record = bestInput as Record<string, unknown>;
1483
+ if (Array.isArray(record.edits)) {
1484
+ bodyLines.push(`edits: ${record.edits.length}`);
1485
+ }
1486
+ }
1487
+
1488
+ if (this.output === undefined) {
1489
+ if (bodyLines.length > 0) {
1490
+ bodyLines.push("");
1491
+ }
1492
+ bodyLines.push(renderPendingOutput());
1493
+ } else {
1494
+ if (bodyLines.length > 0) {
1495
+ bodyLines.push("");
1496
+ }
1497
+ bodyLines.push(...buildTextPreviewLines(safeStringify(this.output)));
1498
+ }
1499
+
1500
+ const editPayload = tryExtractEditPayload(this.toolName, bestInput);
1501
+ if (editPayload) {
1502
+ bodyLines.push("");
1503
+ bodyLines.push("Live diff preview:");
1504
+ bodyLines.push(renderDiffBlock(editPayload.oldStr, editPayload.newStr));
1505
+ }
1506
+
1507
+ this.setPrettyBlock(header, bodyLines.join("\n"), {
1508
+ isPending: this.output === undefined,
1509
+ useBackground: this.output !== undefined,
1510
+ });
1511
+ return true;
1512
+ }
1513
+
1514
+ private tryRenderDeleteFileMode(): boolean {
1515
+ if (!this.canRenderPrettyTool("delete_file")) {
1516
+ return false;
1517
+ }
1518
+
1519
+ const path = this.resolveInputStringField("path") ?? "(unknown)";
1520
+ const header = buildPrettyHeader("Delete", path);
1521
+
1522
+ if (this.output === undefined) {
1523
+ this.setPrettyBlock(header, renderPendingOutput(), {
1524
+ isPending: true,
1525
+ useBackground: false,
1526
+ });
1527
+ return true;
1528
+ }
1529
+
1530
+ this.setPrettyBlock(header, this.renderOutputPreviewBody(this.output));
1531
+ return true;
1532
+ }
1533
+
1534
+ private tryRenderLoadSkillMode(): boolean {
1535
+ if (!this.canRenderPrettyTool("load_skill")) {
1536
+ return false;
1537
+ }
1538
+
1539
+ const skillName = this.resolveInputStringField("skillName") ?? "(unknown)";
1540
+ const relativePath = this.resolveInputStringField("relativePath");
1541
+ const target = relativePath ? `${skillName}/${relativePath}` : skillName;
1542
+ const header = buildPrettyHeader("Skill", target);
1543
+
1544
+ if (this.output === undefined) {
1545
+ this.setPrettyBlock(header, renderPendingOutput(), {
1546
+ isPending: true,
1547
+ useBackground: false,
1548
+ });
1549
+ return true;
1550
+ }
1551
+
1552
+ this.setPrettyBlock(header, this.renderOutputPreviewBody(this.output));
1553
+ return true;
1554
+ }
1555
+
1556
+ private tryRenderTodoWriteMode(): boolean {
1557
+ if (!this.canRenderPrettyTool("todo_write")) {
1558
+ return false;
1559
+ }
1560
+
1561
+ const bestInput = this.resolveBestInput();
1562
+ const todoItems =
1563
+ typeof bestInput === "object" && bestInput !== null
1564
+ ? (bestInput as Record<string, unknown>).todos
1565
+ : undefined;
1566
+ const todos = Array.isArray(todoItems)
1567
+ ? todoItems.filter((item): item is Record<string, unknown> => {
1568
+ return typeof item === "object" && item !== null;
1569
+ })
1570
+ : [];
1571
+
1572
+ const totalTodos = todos.length;
1573
+ const headerTarget = `${totalTodos} task${totalTodos === 1 ? "" : "s"}`;
1574
+ const header = buildPrettyHeader("Todo", headerTarget);
1575
+
1576
+ if (this.output === undefined) {
1577
+ this.setPrettyBlock(header, renderPendingOutput(), {
1578
+ isPending: true,
1579
+ useBackground: false,
1580
+ });
1581
+ return true;
1582
+ }
1583
+
1584
+ const counts = {
1585
+ completed: 0,
1586
+ inProgress: 0,
1587
+ pending: 0,
1588
+ cancelled: 0,
1589
+ };
1590
+
1591
+ for (const todo of todos) {
1592
+ const status =
1593
+ typeof todo.status === "string" ? todo.status.toLowerCase() : "";
1594
+ if (status === "completed") {
1595
+ counts.completed += 1;
1596
+ } else if (status === "in_progress") {
1597
+ counts.inProgress += 1;
1598
+ } else if (status === "pending") {
1599
+ counts.pending += 1;
1600
+ } else if (status === "cancelled") {
1601
+ counts.cancelled += 1;
1602
+ }
1603
+ }
1604
+
1605
+ const bodyLines = [
1606
+ `total: ${totalTodos}`,
1607
+ `completed: ${counts.completed}`,
1608
+ `in_progress: ${counts.inProgress}`,
1609
+ `pending: ${counts.pending}`,
1610
+ `cancelled: ${counts.cancelled}`,
1611
+ "",
1612
+ ...buildTextPreviewLines(safeStringify(this.output)),
1613
+ ];
1614
+
1615
+ this.setPrettyBlock(header, bodyLines.join("\n"));
1616
+ return true;
1617
+ }
1618
+
1619
+ private tryRenderPrettyMode(): boolean {
1620
+ return (
1621
+ this.tryRenderReadFileMode() ||
1622
+ this.tryRenderGlobMode() ||
1623
+ this.tryRenderGrepMode() ||
1624
+ this.tryRenderShellExecuteMode() ||
1625
+ this.tryRenderShellInteractMode() ||
1626
+ this.tryRenderWriteFileMode() ||
1627
+ this.tryRenderEditFileMode() ||
1628
+ this.tryRenderDeleteFileMode() ||
1629
+ this.tryRenderLoadSkillMode() ||
1630
+ this.tryRenderTodoWriteMode()
1631
+ );
1632
+ }
1633
+
1634
+ private shouldSuppressRawFallback(): boolean {
1635
+ if (this.showRawToolIo) {
1636
+ return false;
1637
+ }
1638
+
1639
+ return (
1640
+ this.finalInput === undefined &&
1641
+ this.output === undefined &&
1642
+ this.error === undefined &&
1643
+ !this.outputDenied &&
1644
+ this.inputBuffer.length > 0
1645
+ );
1646
+ }
1647
+
1648
+ private refresh(): void {
1649
+ if (!this.showRawToolIo && this.tryRenderPrettyMode()) {
1650
+ return;
1651
+ }
1652
+
1653
+ this.stopPendingSpinner();
1654
+
1655
+ if (this.shouldSuppressRawFallback()) {
1656
+ return;
1657
+ }
1658
+
1659
+ this.setReadMode(false);
1660
+
1661
+ const includeCallIdInRawHeader = !SHELL_TOOL_NAMES.has(this.toolName);
1662
+ const rawHeader = includeCallIdInRawHeader
1663
+ ? `**Tool** \`${this.toolName}\` (\`${this.callId}\`)`
1664
+ : `**Tool** \`${this.toolName}\``;
1665
+
1666
+ const blocks: string[] = [rawHeader];
1667
+
1668
+ const bestInput = this.resolveBestInput();
1669
+ if (bestInput !== undefined) {
1670
+ blocks.push(`**Input**\n\n${renderCodeBlock("json", bestInput)}`);
1671
+
1672
+ const editPayload = tryExtractEditPayload(this.toolName, bestInput);
1673
+ if (editPayload) {
1674
+ const heading = editPayload.path
1675
+ ? `**Live diff preview** (\`${editPayload.path}\`)`
1676
+ : "**Live diff preview**";
1677
+ blocks.push(
1678
+ `${heading}\n\n${renderDiffBlock(editPayload.oldStr, editPayload.newStr)}`
1679
+ );
1680
+ }
1681
+ }
1682
+
1683
+ if (this.output !== undefined) {
1684
+ blocks.push(
1685
+ `**Output**\n\n${renderToolOutput(this.toolName, this.output)}`
1686
+ );
1687
+ }
1688
+
1689
+ if (this.error !== undefined) {
1690
+ blocks.push(`**Error**\n\n${renderCodeBlock("text", this.error)}`);
1691
+ }
1692
+
1693
+ if (this.outputDenied) {
1694
+ blocks.push("**Output** denied by model/policy");
1695
+ }
1696
+
1697
+ this.content.setText(blocks.join("\n\n"));
1698
+ }
1699
+ }
1700
+
1701
+ const getToolInputId = (part: ToolInputPart): string | undefined => {
1702
+ return part.id ?? part.toolCallId;
1703
+ };
1704
+
1705
+ const getToolInputChunk = (part: ToolInputDeltaPart): string | null => {
1706
+ if (typeof part.delta === "string") {
1707
+ return part.delta;
1708
+ }
1709
+
1710
+ if (typeof part.inputTextDelta === "string") {
1711
+ return part.inputTextDelta;
1712
+ }
1713
+
1714
+ return null;
1715
+ };
1716
+
1717
+ const createToolInputState = (toolName: string): ToolInputRenderState => {
1718
+ return {
1719
+ toolName,
1720
+ hasContent: false,
1721
+ inputBuffer: "",
1722
+ renderedInputLength: 0,
1723
+ };
1724
+ };
1725
+
1726
+ const syncToolInputToView = async (
1727
+ state: PiTuiStreamState,
1728
+ toolCallId: string,
1729
+ toolState: ToolInputRenderState
1730
+ ): Promise<void> => {
1731
+ const hasKnownToolName = toolState.toolName !== UNKNOWN_TOOL_NAME;
1732
+ if (!(state.flags.showRawToolIo || hasKnownToolName)) {
1733
+ return;
1734
+ }
1735
+
1736
+ const existingView = state.getToolView(toolCallId);
1737
+ const pendingInput = toolState.inputBuffer.slice(
1738
+ toolState.renderedInputLength
1739
+ );
1740
+ if (!existingView && pendingInput.length === 0) {
1741
+ return;
1742
+ }
1743
+
1744
+ state.resetAssistantView(true);
1745
+ const toolView =
1746
+ existingView ?? state.ensureToolView(toolCallId, toolState.toolName);
1747
+
1748
+ if (pendingInput.length === 0) {
1749
+ return;
1750
+ }
1751
+
1752
+ await toolView.appendInputChunk(pendingInput);
1753
+ toolState.renderedInputLength = toolState.inputBuffer.length;
1754
+ };
1755
+
1756
+ const createInfoMessage = (title: string, value: unknown): Text => {
1757
+ return new Text(`${title}\n${safeStringify(value)}`, 1, 0);
1758
+ };
1759
+
1760
+ interface PiTuiRenderFlags {
1761
+ showFiles: boolean;
1762
+ showFinishReason: boolean;
1763
+ showRawToolIo: boolean;
1764
+ showReasoning: boolean;
1765
+ showSources: boolean;
1766
+ showSteps: boolean;
1767
+ showToolResults: boolean;
1768
+ }
1769
+
1770
+ interface PiTuiStreamState {
1771
+ activeToolInputs: Map<string, ToolInputRenderState>;
1772
+ chatContainer: Container;
1773
+ ensureAssistantView: () => AssistantStreamView;
1774
+ ensureToolView: (toolCallId: string, toolName: string) => ToolCallView;
1775
+ flags: PiTuiRenderFlags;
1776
+ getToolView: (toolCallId: string) => ToolCallView | undefined;
1777
+ resetAssistantView: (suppressLeadingSpacer?: boolean) => void;
1778
+ streamedToolCallIds: Set<string>;
1779
+ }
1780
+
1781
+ type StreamPartHandler = (
1782
+ part: StreamPart,
1783
+ state: PiTuiStreamState
1784
+ ) => void | Promise<void>;
1785
+
1786
+ const handleTextStart: StreamPartHandler = (_part, state) => {
1787
+ state.ensureAssistantView();
1788
+ };
1789
+
1790
+ const handleTextDelta: StreamPartHandler = (part, state) => {
1791
+ const textPart = part as Extract<StreamPart, { type: "text-delta" }>;
1792
+ state.ensureAssistantView().appendText(textPart.text);
1793
+ };
1794
+
1795
+ const handleReasoningStart: StreamPartHandler = (_part, state) => {
1796
+ if (state.flags.showReasoning) {
1797
+ state.ensureAssistantView();
1798
+ }
1799
+ };
1800
+
1801
+ const handleReasoningDelta: StreamPartHandler = (part, state) => {
1802
+ if (!state.flags.showReasoning) {
1803
+ return;
1804
+ }
1805
+ const reasoningPart = part as Extract<
1806
+ StreamPart,
1807
+ { type: "reasoning-delta" }
1808
+ >;
1809
+ state.ensureAssistantView().appendReasoning(reasoningPart.text);
1810
+ };
1811
+
1812
+ const handleToolInputStart: StreamPartHandler = async (part, state) => {
1813
+ const toolInputStartPart = part as Extract<
1814
+ StreamPart,
1815
+ { type: "tool-input-start" }
1816
+ >;
1817
+ const toolCallId = getToolInputId(toolInputStartPart);
1818
+ if (!toolCallId) {
1819
+ return;
1820
+ }
1821
+
1822
+ const existingState = state.activeToolInputs.get(toolCallId);
1823
+ const toolState =
1824
+ existingState ?? createToolInputState(toolInputStartPart.toolName);
1825
+ toolState.toolName = toolInputStartPart.toolName;
1826
+
1827
+ state.activeToolInputs.set(toolCallId, toolState);
1828
+ state.streamedToolCallIds.add(toolCallId);
1829
+ await syncToolInputToView(state, toolCallId, toolState);
1830
+ };
1831
+
1832
+ const handleToolInputDelta: StreamPartHandler = async (part, state) => {
1833
+ const toolInputDeltaPart = part as Extract<
1834
+ StreamPart,
1835
+ { type: "tool-input-delta" }
1836
+ >;
1837
+ const toolCallId = getToolInputId(toolInputDeltaPart);
1838
+ if (!toolCallId) {
1839
+ return;
1840
+ }
1841
+
1842
+ if (!state.activeToolInputs.has(toolCallId)) {
1843
+ state.activeToolInputs.set(
1844
+ toolCallId,
1845
+ createToolInputState(UNKNOWN_TOOL_NAME)
1846
+ );
1847
+ }
1848
+
1849
+ const toolState = state.activeToolInputs.get(toolCallId);
1850
+ const chunk = getToolInputChunk(toolInputDeltaPart);
1851
+
1852
+ if (chunk && toolState) {
1853
+ toolState.inputBuffer += chunk;
1854
+ toolState.hasContent = true;
1855
+ await syncToolInputToView(state, toolCallId, toolState);
1856
+ }
1857
+
1858
+ state.streamedToolCallIds.add(toolCallId);
1859
+ };
1860
+
1861
+ const handleToolInputEnd: StreamPartHandler = (part, state) => {
1862
+ const toolInputEndPart = part as Extract<
1863
+ StreamPart,
1864
+ { type: "tool-input-end" }
1865
+ >;
1866
+ const toolCallId = getToolInputId(toolInputEndPart);
1867
+ if (toolCallId) {
1868
+ state.streamedToolCallIds.add(toolCallId);
1869
+ }
1870
+ };
1871
+
1872
+ const handleToolCall: StreamPartHandler = (part, state) => {
1873
+ const toolCallPart = part as Extract<StreamPart, { type: "tool-call" }>;
1874
+ const inputState = state.activeToolInputs.get(toolCallPart.toolCallId);
1875
+ const shouldSkipToolCallRender =
1876
+ state.streamedToolCallIds.has(toolCallPart.toolCallId) &&
1877
+ inputState?.hasContent === true;
1878
+
1879
+ state.activeToolInputs.delete(toolCallPart.toolCallId);
1880
+ state.streamedToolCallIds.delete(toolCallPart.toolCallId);
1881
+
1882
+ state.resetAssistantView(true);
1883
+ const view = state.ensureToolView(
1884
+ toolCallPart.toolCallId,
1885
+ toolCallPart.toolName
1886
+ );
1887
+ view.setFinalInput(toolCallPart.input);
1888
+
1889
+ if (!shouldSkipToolCallRender) {
1890
+ view.setToolName(toolCallPart.toolName);
1891
+ }
1892
+ };
1893
+
1894
+ const handleToolResult: StreamPartHandler = (part, state) => {
1895
+ if (!state.flags.showToolResults) {
1896
+ return;
1897
+ }
1898
+
1899
+ const toolResultPart = part as Extract<StreamPart, { type: "tool-result" }>;
1900
+ state.resetAssistantView(true);
1901
+ const view = state.ensureToolView(
1902
+ toolResultPart.toolCallId,
1903
+ toolResultPart.toolName
1904
+ );
1905
+ view.setOutput(toolResultPart.output);
1906
+ };
1907
+
1908
+ const handleToolError: StreamPartHandler = (part, state) => {
1909
+ const toolErrorPart = part as Extract<StreamPart, { type: "tool-error" }>;
1910
+ state.resetAssistantView(true);
1911
+ const view = state.ensureToolView(
1912
+ toolErrorPart.toolCallId,
1913
+ toolErrorPart.toolName
1914
+ );
1915
+ view.setError(toolErrorPart.error);
1916
+ };
1917
+
1918
+ const handleToolOutputDenied: StreamPartHandler = (part, state) => {
1919
+ const deniedPart = part as Extract<
1920
+ StreamPart,
1921
+ { type: "tool-output-denied" }
1922
+ >;
1923
+ state.resetAssistantView(true);
1924
+ const view = state.ensureToolView(deniedPart.toolCallId, deniedPart.toolName);
1925
+ view.setOutputDenied();
1926
+ };
1927
+
1928
+ const handleStartStep: StreamPartHandler = (_part, state) => {
1929
+ if (!state.flags.showSteps) {
1930
+ return;
1931
+ }
1932
+ state.resetAssistantView();
1933
+ addChatComponent(state.chatContainer, createInfoMessage("[step start]", ""));
1934
+ };
1935
+
1936
+ const handleFinishStep: StreamPartHandler = (part, state) => {
1937
+ if (!state.flags.showSteps) {
1938
+ return;
1939
+ }
1940
+ const finishStepPart = part as Extract<StreamPart, { type: "finish-step" }>;
1941
+ state.resetAssistantView();
1942
+ addChatComponent(
1943
+ state.chatContainer,
1944
+ createInfoMessage("[step finish]", finishStepPart.finishReason)
1945
+ );
1946
+ };
1947
+
1948
+ const handleSource: StreamPartHandler = (part, state) => {
1949
+ if (!state.flags.showSources) {
1950
+ return;
1951
+ }
1952
+ state.resetAssistantView();
1953
+ addChatComponent(state.chatContainer, createInfoMessage("[source]", part));
1954
+ };
1955
+
1956
+ const handleFile: StreamPartHandler = (part, state) => {
1957
+ if (!state.flags.showFiles) {
1958
+ return;
1959
+ }
1960
+ const filePart = part as Extract<StreamPart, { type: "file" }>;
1961
+ state.resetAssistantView();
1962
+ addChatComponent(
1963
+ state.chatContainer,
1964
+ createInfoMessage("[file]", filePart.file)
1965
+ );
1966
+ };
1967
+
1968
+ const handleFinish: StreamPartHandler = (part, state) => {
1969
+ if (!state.flags.showFinishReason) {
1970
+ return;
1971
+ }
1972
+
1973
+ const finishPart = part as Extract<StreamPart, { type: "finish" }>;
1974
+ state.resetAssistantView();
1975
+ addChatComponent(
1976
+ state.chatContainer,
1977
+ createInfoMessage("[finish]", finishPart.finishReason ?? "unknown")
1978
+ );
1979
+ };
1980
+
1981
+ const STREAM_HANDLERS: Record<string, StreamPartHandler> = {
1982
+ "text-start": handleTextStart,
1983
+ "text-delta": handleTextDelta,
1984
+ "reasoning-start": handleReasoningStart,
1985
+ "reasoning-delta": handleReasoningDelta,
1986
+ "tool-input-start": handleToolInputStart,
1987
+ "tool-input-delta": handleToolInputDelta,
1988
+ "tool-input-end": handleToolInputEnd,
1989
+ "tool-call": handleToolCall,
1990
+ "tool-result": handleToolResult,
1991
+ "tool-error": handleToolError,
1992
+ "tool-output-denied": handleToolOutputDenied,
1993
+ "start-step": handleStartStep,
1994
+ "finish-step": handleFinishStep,
1995
+ source: handleSource,
1996
+ file: handleFile,
1997
+ finish: handleFinish,
1998
+ };
1999
+
2000
+ const IGNORE_PART_TYPES = new Set([
2001
+ "abort",
2002
+ "text-end",
2003
+ "reasoning-end",
2004
+ "start",
2005
+ "tool-approval-request",
2006
+ ]);
2007
+
2008
+ const isVisibleStreamPart = (
2009
+ part: StreamPart,
2010
+ flags: PiTuiRenderFlags
2011
+ ): boolean => {
2012
+ switch (part.type) {
2013
+ case "abort":
2014
+ case "text-end":
2015
+ case "reasoning-end":
2016
+ case "start":
2017
+ case "tool-approval-request":
2018
+ case "text-start":
2019
+ case "reasoning-start":
2020
+ case "tool-input-end":
2021
+ return false;
2022
+ case "reasoning-delta":
2023
+ return flags.showReasoning;
2024
+ case "tool-result":
2025
+ return flags.showToolResults;
2026
+ case "start-step":
2027
+ case "finish-step":
2028
+ return flags.showSteps;
2029
+ case "source":
2030
+ return flags.showSources;
2031
+ case "file":
2032
+ return flags.showFiles;
2033
+ case "finish":
2034
+ return flags.showFinishReason;
2035
+ default:
2036
+ return true;
2037
+ }
2038
+ };
2039
+
2040
+ const handleStreamPart = async (
2041
+ part: StreamPart,
2042
+ state: PiTuiStreamState
2043
+ ): Promise<void> => {
2044
+ const handler = STREAM_HANDLERS[part.type];
2045
+ if (handler) {
2046
+ await handler(part, state);
2047
+ return;
2048
+ }
2049
+
2050
+ if (IGNORE_PART_TYPES.has(part.type)) {
2051
+ return;
2052
+ }
2053
+
2054
+ state.resetAssistantView();
2055
+ addChatComponent(
2056
+ state.chatContainer,
2057
+ createInfoMessage("[unknown part]", part)
2058
+ );
2059
+ };
2060
+
2061
+ export const renderFullStreamWithPiTui = async <TOOLS extends ToolSet>(
2062
+ stream: AsyncIterable<TextStreamPart<TOOLS>>,
2063
+ options: PiTuiStreamRenderOptions
2064
+ ): Promise<void> => {
2065
+ const flags: PiTuiRenderFlags = {
2066
+ showReasoning: options.showReasoning ?? true,
2067
+ showSteps: options.showSteps ?? false,
2068
+ showFinishReason: options.showFinishReason ?? false,
2069
+ showRawToolIo: options.showRawToolIo ?? isRawToolIoEnabledByEnv(),
2070
+ showToolResults: options.showToolResults ?? true,
2071
+ showSources: options.showSources ?? false,
2072
+ showFiles: options.showFiles ?? false,
2073
+ };
2074
+
2075
+ const activeToolInputs = new Map<string, ToolInputRenderState>();
2076
+ const streamedToolCallIds = new Set<string>();
2077
+ const toolViews = new Map<string, ToolCallView>();
2078
+ let assistantView: AssistantStreamView | null = null;
2079
+ let suppressAssistantLeadingSpacer = false;
2080
+ let firstVisiblePartSeen = false;
2081
+
2082
+ const resetAssistantView = (suppressLeadingSpacer = false): void => {
2083
+ if (suppressLeadingSpacer) {
2084
+ suppressAssistantLeadingSpacer = true;
2085
+ }
2086
+ assistantView = null;
2087
+ };
2088
+
2089
+ const ensureAssistantView = (): AssistantStreamView => {
2090
+ if (!assistantView) {
2091
+ assistantView = new AssistantStreamView(options.markdownTheme);
2092
+ addChatComponent(options.chatContainer, assistantView, {
2093
+ addLeadingSpacer: !suppressAssistantLeadingSpacer,
2094
+ });
2095
+ suppressAssistantLeadingSpacer = false;
2096
+ }
2097
+ return assistantView;
2098
+ };
2099
+
2100
+ const ensureToolView = (
2101
+ toolCallId: string,
2102
+ toolName: string
2103
+ ): ToolCallView => {
2104
+ const existing = toolViews.get(toolCallId);
2105
+ if (existing) {
2106
+ existing.setToolName(toolName);
2107
+ return existing;
2108
+ }
2109
+
2110
+ const view = new ToolCallView(
2111
+ toolCallId,
2112
+ toolName,
2113
+ options.markdownTheme,
2114
+ () => options.ui.requestRender(),
2115
+ flags.showRawToolIo
2116
+ );
2117
+ toolViews.set(toolCallId, view);
2118
+ addChatComponent(options.chatContainer, view);
2119
+ return view;
2120
+ };
2121
+
2122
+ const state: PiTuiStreamState = {
2123
+ flags,
2124
+ activeToolInputs,
2125
+ streamedToolCallIds,
2126
+ resetAssistantView,
2127
+ ensureAssistantView,
2128
+ ensureToolView,
2129
+ getToolView: (toolCallId: string) => toolViews.get(toolCallId),
2130
+ chatContainer: options.chatContainer,
2131
+ };
2132
+
2133
+ try {
2134
+ for await (const rawPart of stream) {
2135
+ const part = rawPart as StreamPart;
2136
+
2137
+ if (!firstVisiblePartSeen && isVisibleStreamPart(part, flags)) {
2138
+ firstVisiblePartSeen = true;
2139
+ options.onFirstVisiblePart?.();
2140
+ }
2141
+
2142
+ await handleStreamPart(part, state);
2143
+ options.ui.requestRender();
2144
+ }
2145
+ } finally {
2146
+ for (const view of toolViews.values()) {
2147
+ view.dispose();
2148
+ }
2149
+ }
2150
+ };