lonny-agent 0.2.6 → 0.2.7

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 (475) hide show
  1. package/.github/workflows/ci.yml +21 -0
  2. package/AGENTS.md +106 -0
  3. package/CHANGELOG.md +24 -0
  4. package/dist/agent/__tests__/session-restore.test.d.ts +2 -0
  5. package/dist/agent/__tests__/session-restore.test.d.ts.map +1 -0
  6. package/dist/agent/__tests__/session-restore.test.js +260 -0
  7. package/dist/agent/__tests__/session-restore.test.js.map +1 -0
  8. package/dist/agent/commands.d.ts +26 -0
  9. package/dist/agent/commands.d.ts.map +1 -0
  10. package/dist/agent/commands.js +253 -0
  11. package/dist/agent/commands.js.map +1 -0
  12. package/dist/agent/compaction.js +8 -1
  13. package/dist/agent/compaction.js.map +1 -1
  14. package/dist/agent/index.d.ts +2 -1
  15. package/dist/agent/index.d.ts.map +1 -1
  16. package/dist/agent/index.js +2 -2
  17. package/dist/agent/index.js.map +1 -1
  18. package/dist/agent/memory.d.ts +12 -0
  19. package/dist/agent/memory.d.ts.map +1 -0
  20. package/dist/agent/memory.js +51 -0
  21. package/dist/agent/memory.js.map +1 -0
  22. package/dist/agent/prompt-builder.d.ts +6 -1
  23. package/dist/agent/prompt-builder.d.ts.map +1 -1
  24. package/dist/agent/prompt-builder.js +89 -26
  25. package/dist/agent/prompt-builder.js.map +1 -1
  26. package/dist/agent/providers/openai.d.ts +1 -0
  27. package/dist/agent/providers/openai.d.ts.map +1 -1
  28. package/dist/agent/providers/openai.js +16 -3
  29. package/dist/agent/providers/openai.js.map +1 -1
  30. package/dist/agent/session-chat.d.ts +3 -0
  31. package/dist/agent/session-chat.d.ts.map +1 -0
  32. package/dist/agent/session-chat.js +384 -0
  33. package/dist/agent/session-chat.js.map +1 -0
  34. package/dist/agent/session-display.d.ts +29 -0
  35. package/dist/agent/session-display.d.ts.map +1 -0
  36. package/dist/agent/session-display.js +179 -0
  37. package/dist/agent/session-display.js.map +1 -0
  38. package/dist/agent/session-persistence.d.ts +55 -0
  39. package/dist/agent/session-persistence.d.ts.map +1 -0
  40. package/dist/agent/session-persistence.js +101 -0
  41. package/dist/agent/session-persistence.js.map +1 -0
  42. package/dist/agent/session-utils.d.ts +5 -0
  43. package/dist/agent/session-utils.d.ts.map +1 -0
  44. package/dist/agent/session-utils.js +51 -0
  45. package/dist/agent/session-utils.js.map +1 -0
  46. package/dist/agent/session.d.ts +15 -17
  47. package/dist/agent/session.d.ts.map +1 -1
  48. package/dist/agent/session.js +226 -608
  49. package/dist/agent/session.js.map +1 -1
  50. package/dist/cli/index.d.ts +4 -0
  51. package/dist/cli/index.d.ts.map +1 -1
  52. package/dist/cli/index.js +67 -2
  53. package/dist/cli/index.js.map +1 -1
  54. package/dist/config/index.d.ts +2 -2
  55. package/dist/config/index.d.ts.map +1 -1
  56. package/dist/index.js +24 -3
  57. package/dist/index.js.map +1 -1
  58. package/dist/tools/__tests__/bash.test.js +168 -1
  59. package/dist/tools/__tests__/bash.test.js.map +1 -1
  60. package/dist/tools/__tests__/edit.test.js +253 -6
  61. package/dist/tools/__tests__/edit.test.js.map +1 -1
  62. package/dist/tools/__tests__/fetch.test.js +1 -1
  63. package/dist/tools/__tests__/fetch.test.js.map +1 -1
  64. package/dist/tools/__tests__/glob.test.js +1 -1
  65. package/dist/tools/__tests__/glob.test.js.map +1 -1
  66. package/dist/tools/__tests__/grep.test.js +1 -1
  67. package/dist/tools/__tests__/grep.test.js.map +1 -1
  68. package/dist/tools/__tests__/ls.test.js +1 -1
  69. package/dist/tools/__tests__/ls.test.js.map +1 -1
  70. package/dist/tools/__tests__/read.test.js +1 -1
  71. package/dist/tools/__tests__/read.test.js.map +1 -1
  72. package/dist/tools/__tests__/registry.test.js +313 -0
  73. package/dist/tools/__tests__/registry.test.js.map +1 -1
  74. package/dist/tools/__tests__/write_plan.test.js +1 -1
  75. package/dist/tools/__tests__/write_plan.test.js.map +1 -1
  76. package/dist/tools/bash.d.ts.map +1 -1
  77. package/dist/tools/bash.js +659 -39
  78. package/dist/tools/bash.js.map +1 -1
  79. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts +2 -0
  80. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts.map +1 -0
  81. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js +7 -0
  82. package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js.map +1 -0
  83. package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts +6 -0
  84. package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts.map +1 -0
  85. package/dist/tools/codebase/ast/__tests__/ast-edit-final.js +11 -0
  86. package/dist/tools/codebase/ast/__tests__/ast-edit-final.js.map +1 -0
  87. package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts +2 -0
  88. package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts.map +1 -0
  89. package/dist/tools/codebase/ast/__tests__/ast-edit-test.js +9 -0
  90. package/dist/tools/codebase/ast/__tests__/ast-edit-test.js.map +1 -0
  91. package/dist/tools/codebase/ast/__tests__/integration.test.d.ts +2 -0
  92. package/dist/tools/codebase/ast/__tests__/integration.test.d.ts.map +1 -0
  93. package/dist/tools/codebase/ast/__tests__/integration.test.js +415 -0
  94. package/dist/tools/codebase/ast/__tests__/integration.test.js.map +1 -0
  95. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts +2 -0
  96. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts.map +1 -0
  97. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js +414 -0
  98. package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js.map +1 -0
  99. package/dist/tools/codebase/ast/adapter.d.ts +23 -0
  100. package/dist/tools/codebase/ast/adapter.d.ts.map +1 -0
  101. package/dist/tools/codebase/ast/adapter.js +11 -0
  102. package/dist/tools/codebase/ast/adapter.js.map +1 -0
  103. package/dist/tools/codebase/ast/tools.d.ts +3 -0
  104. package/dist/tools/codebase/ast/tools.d.ts.map +1 -0
  105. package/dist/tools/codebase/ast/tools.js +286 -0
  106. package/dist/tools/codebase/ast/tools.js.map +1 -0
  107. package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts +3 -0
  108. package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts.map +1 -0
  109. package/dist/tools/codebase/ast/tree-sitter-adapter.js +412 -0
  110. package/dist/tools/codebase/ast/tree-sitter-adapter.js.map +1 -0
  111. package/dist/tools/codebase/ast/tree-sitter-converters.d.ts +4 -0
  112. package/dist/tools/codebase/ast/tree-sitter-converters.d.ts.map +1 -0
  113. package/dist/tools/codebase/ast/tree-sitter-converters.js +447 -0
  114. package/dist/tools/codebase/ast/tree-sitter-converters.js.map +1 -0
  115. package/dist/tools/codebase/ast/types.d.ts +103 -0
  116. package/dist/tools/codebase/ast/types.d.ts.map +1 -0
  117. package/dist/tools/codebase/ast/types.js +2 -0
  118. package/dist/tools/codebase/ast/types.js.map +1 -0
  119. package/dist/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
  120. package/dist/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
  121. package/dist/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
  122. package/dist/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
  123. package/dist/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
  124. package/dist/tools/codebase/find.d.ts +3 -0
  125. package/dist/tools/codebase/find.d.ts.map +1 -0
  126. package/dist/tools/codebase/find.js +61 -0
  127. package/dist/tools/codebase/find.js.map +1 -0
  128. package/dist/tools/codebase/glob.d.ts +3 -0
  129. package/dist/tools/codebase/glob.d.ts.map +1 -0
  130. package/dist/tools/codebase/glob.js +37 -0
  131. package/dist/tools/codebase/glob.js.map +1 -0
  132. package/dist/tools/codebase/grep.d.ts +3 -0
  133. package/dist/tools/codebase/grep.d.ts.map +1 -0
  134. package/dist/tools/codebase/grep.js +169 -0
  135. package/dist/tools/codebase/grep.js.map +1 -0
  136. package/dist/tools/codebase/ls.d.ts +3 -0
  137. package/dist/tools/codebase/ls.d.ts.map +1 -0
  138. package/dist/tools/codebase/ls.js +39 -0
  139. package/dist/tools/codebase/ls.js.map +1 -0
  140. package/dist/tools/codebase/read.d.ts +10 -0
  141. package/dist/tools/codebase/read.d.ts.map +1 -0
  142. package/dist/tools/codebase/read.js +81 -0
  143. package/dist/tools/codebase/read.js.map +1 -0
  144. package/dist/tools/delete_memory.d.ts +3 -0
  145. package/dist/tools/delete_memory.d.ts.map +1 -0
  146. package/dist/tools/delete_memory.js +46 -0
  147. package/dist/tools/delete_memory.js.map +1 -0
  148. package/dist/tools/edit/diff-compute.d.ts +4 -0
  149. package/dist/tools/edit/diff-compute.d.ts.map +1 -0
  150. package/dist/tools/edit/diff-compute.js +18 -0
  151. package/dist/tools/edit/diff-compute.js.map +1 -0
  152. package/dist/tools/edit/diff-render.d.ts +18 -0
  153. package/dist/tools/edit/diff-render.d.ts.map +1 -0
  154. package/dist/tools/edit/diff-render.js +128 -0
  155. package/dist/tools/edit/diff-render.js.map +1 -0
  156. package/dist/tools/edit/edit.d.ts +4 -0
  157. package/dist/tools/edit/edit.d.ts.map +1 -0
  158. package/dist/tools/edit/edit.js +440 -0
  159. package/dist/tools/edit/edit.js.map +1 -0
  160. package/dist/tools/edit/matcher.d.ts +24 -0
  161. package/dist/tools/edit/matcher.d.ts.map +1 -0
  162. package/dist/tools/edit/matcher.js +66 -0
  163. package/dist/tools/edit/matcher.js.map +1 -0
  164. package/dist/tools/edit/parser.d.ts +10 -0
  165. package/dist/tools/edit/parser.d.ts.map +1 -0
  166. package/dist/tools/edit/parser.js +120 -0
  167. package/dist/tools/edit/parser.js.map +1 -0
  168. package/dist/tools/edit/types.d.ts +16 -0
  169. package/dist/tools/edit/types.d.ts.map +1 -0
  170. package/dist/tools/edit/types.js +2 -0
  171. package/dist/tools/edit/types.js.map +1 -0
  172. package/dist/tools/edit/write_plan.d.ts +5 -0
  173. package/dist/tools/edit/write_plan.d.ts.map +1 -0
  174. package/dist/tools/edit/write_plan.js +98 -0
  175. package/dist/tools/edit/write_plan.js.map +1 -0
  176. package/dist/tools/edit.d.ts.map +1 -1
  177. package/dist/tools/edit.js +66 -1
  178. package/dist/tools/edit.js.map +1 -1
  179. package/dist/tools/execute/bash/constants.d.ts +24 -0
  180. package/dist/tools/execute/bash/constants.d.ts.map +1 -0
  181. package/dist/tools/execute/bash/constants.js +88 -0
  182. package/dist/tools/execute/bash/constants.js.map +1 -0
  183. package/dist/tools/execute/bash/errors.d.ts +39 -0
  184. package/dist/tools/execute/bash/errors.d.ts.map +1 -0
  185. package/dist/tools/execute/bash/errors.js +249 -0
  186. package/dist/tools/execute/bash/errors.js.map +1 -0
  187. package/dist/tools/execute/bash/execution.d.ts +18 -0
  188. package/dist/tools/execute/bash/execution.d.ts.map +1 -0
  189. package/dist/tools/execute/bash/execution.js +207 -0
  190. package/dist/tools/execute/bash/execution.js.map +1 -0
  191. package/dist/tools/execute/bash/index.d.ts +3 -0
  192. package/dist/tools/execute/bash/index.d.ts.map +1 -0
  193. package/dist/tools/execute/bash/index.js +117 -0
  194. package/dist/tools/execute/bash/index.js.map +1 -0
  195. package/dist/tools/execute/bash/platform.d.ts +6 -0
  196. package/dist/tools/execute/bash/platform.d.ts.map +1 -0
  197. package/dist/tools/execute/bash/platform.js +64 -0
  198. package/dist/tools/execute/bash/platform.js.map +1 -0
  199. package/dist/tools/execute/bash/security.d.ts +12 -0
  200. package/dist/tools/execute/bash/security.d.ts.map +1 -0
  201. package/dist/tools/execute/bash/security.js +26 -0
  202. package/dist/tools/execute/bash/security.js.map +1 -0
  203. package/dist/tools/execute/bash/validation.d.ts +16 -0
  204. package/dist/tools/execute/bash/validation.d.ts.map +1 -0
  205. package/dist/tools/execute/bash/validation.js +57 -0
  206. package/dist/tools/execute/bash/validation.js.map +1 -0
  207. package/dist/tools/execute/bash.d.ts +3 -0
  208. package/dist/tools/execute/bash.d.ts.map +1 -0
  209. package/dist/tools/execute/bash.js +728 -0
  210. package/dist/tools/execute/bash.js.map +1 -0
  211. package/dist/tools/execute/git.d.ts +3 -0
  212. package/dist/tools/execute/git.d.ts.map +1 -0
  213. package/dist/tools/execute/git.js +183 -0
  214. package/dist/tools/execute/git.js.map +1 -0
  215. package/dist/tools/execute/task_complete.d.ts +7 -0
  216. package/dist/tools/execute/task_complete.d.ts.map +1 -0
  217. package/dist/tools/execute/task_complete.js +38 -0
  218. package/dist/tools/execute/task_complete.js.map +1 -0
  219. package/dist/tools/fetch.d.ts.map +1 -1
  220. package/dist/tools/fetch.js +2 -0
  221. package/dist/tools/fetch.js.map +1 -1
  222. package/dist/tools/find.d.ts.map +1 -1
  223. package/dist/tools/find.js +2 -0
  224. package/dist/tools/find.js.map +1 -1
  225. package/dist/tools/git.d.ts.map +1 -1
  226. package/dist/tools/git.js +2 -0
  227. package/dist/tools/git.js.map +1 -1
  228. package/dist/tools/glob.d.ts.map +1 -1
  229. package/dist/tools/glob.js +2 -0
  230. package/dist/tools/glob.js.map +1 -1
  231. package/dist/tools/grep.d.ts.map +1 -1
  232. package/dist/tools/grep.js +2 -0
  233. package/dist/tools/grep.js.map +1 -1
  234. package/dist/tools/install/clawhub.d.ts +26 -0
  235. package/dist/tools/install/clawhub.d.ts.map +1 -0
  236. package/dist/tools/install/clawhub.js +182 -0
  237. package/dist/tools/install/clawhub.js.map +1 -0
  238. package/dist/tools/install/constants.d.ts +5 -0
  239. package/dist/tools/install/constants.d.ts.map +1 -0
  240. package/dist/tools/install/constants.js +5 -0
  241. package/dist/tools/install/constants.js.map +1 -0
  242. package/dist/tools/install/install_skill.d.ts +3 -0
  243. package/dist/tools/install/install_skill.d.ts.map +1 -0
  244. package/dist/tools/install/install_skill.js +132 -0
  245. package/dist/tools/install/install_skill.js.map +1 -0
  246. package/dist/tools/install/npm.d.ts +8 -0
  247. package/dist/tools/install/npm.d.ts.map +1 -0
  248. package/dist/tools/install/npm.js +52 -0
  249. package/dist/tools/install/npm.js.map +1 -0
  250. package/dist/tools/install/skill-content.d.ts +9 -0
  251. package/dist/tools/install/skill-content.d.ts.map +1 -0
  252. package/dist/tools/install/skill-content.js +89 -0
  253. package/dist/tools/install/skill-content.js.map +1 -0
  254. package/dist/tools/install_skill.d.ts.map +1 -1
  255. package/dist/tools/install_skill.js +2 -0
  256. package/dist/tools/install_skill.js.map +1 -1
  257. package/dist/tools/list_memory.d.ts +3 -0
  258. package/dist/tools/list_memory.d.ts.map +1 -0
  259. package/dist/tools/list_memory.js +55 -0
  260. package/dist/tools/list_memory.js.map +1 -0
  261. package/dist/tools/ls.d.ts.map +1 -1
  262. package/dist/tools/ls.js +2 -0
  263. package/dist/tools/ls.js.map +1 -1
  264. package/dist/tools/memory/delete_memory.d.ts +3 -0
  265. package/dist/tools/memory/delete_memory.d.ts.map +1 -0
  266. package/dist/tools/memory/delete_memory.js +46 -0
  267. package/dist/tools/memory/delete_memory.js.map +1 -0
  268. package/dist/tools/memory/list_memory.d.ts +3 -0
  269. package/dist/tools/memory/list_memory.d.ts.map +1 -0
  270. package/dist/tools/memory/list_memory.js +55 -0
  271. package/dist/tools/memory/list_memory.js.map +1 -0
  272. package/dist/tools/memory/save_memory.d.ts +3 -0
  273. package/dist/tools/memory/save_memory.d.ts.map +1 -0
  274. package/dist/tools/memory/save_memory.js +45 -0
  275. package/dist/tools/memory/save_memory.js.map +1 -0
  276. package/dist/tools/read.d.ts.map +1 -1
  277. package/dist/tools/read.js +2 -0
  278. package/dist/tools/read.js.map +1 -1
  279. package/dist/tools/registry.d.ts +7 -2
  280. package/dist/tools/registry.d.ts.map +1 -1
  281. package/dist/tools/registry.js +112 -17
  282. package/dist/tools/registry.js.map +1 -1
  283. package/dist/tools/save_memory.d.ts +3 -0
  284. package/dist/tools/save_memory.d.ts.map +1 -0
  285. package/dist/tools/save_memory.js +45 -0
  286. package/dist/tools/save_memory.js.map +1 -0
  287. package/dist/tools/search.d.ts.map +1 -1
  288. package/dist/tools/search.js +2 -0
  289. package/dist/tools/search.js.map +1 -1
  290. package/dist/tools/tree.d.ts +12 -0
  291. package/dist/tools/tree.d.ts.map +1 -0
  292. package/dist/tools/tree.js +87 -0
  293. package/dist/tools/tree.js.map +1 -0
  294. package/dist/tools/types.d.ts +12 -0
  295. package/dist/tools/types.d.ts.map +1 -1
  296. package/dist/tools/web/fetch.d.ts +3 -0
  297. package/dist/tools/web/fetch.d.ts.map +1 -0
  298. package/dist/tools/web/fetch.js +49 -0
  299. package/dist/tools/web/fetch.js.map +1 -0
  300. package/dist/tools/web/search.d.ts +3 -0
  301. package/dist/tools/web/search.d.ts.map +1 -0
  302. package/dist/tools/web/search.js +134 -0
  303. package/dist/tools/web/search.js.map +1 -0
  304. package/dist/tools/write_plan.d.ts.map +1 -1
  305. package/dist/tools/write_plan.js +2 -0
  306. package/dist/tools/write_plan.js.map +1 -1
  307. package/dist/tui/commands.d.ts +4 -0
  308. package/dist/tui/commands.d.ts.map +1 -0
  309. package/dist/tui/commands.js +118 -0
  310. package/dist/tui/commands.js.map +1 -0
  311. package/dist/tui/components/colors.d.ts +36 -0
  312. package/dist/tui/components/colors.d.ts.map +1 -0
  313. package/dist/tui/components/colors.js +48 -0
  314. package/dist/tui/components/colors.js.map +1 -0
  315. package/dist/tui/components/header-bar.d.ts +26 -0
  316. package/dist/tui/components/header-bar.d.ts.map +1 -0
  317. package/dist/tui/components/header-bar.js +78 -0
  318. package/dist/tui/components/header-bar.js.map +1 -0
  319. package/dist/tui/components/index.d.ts +9 -0
  320. package/dist/tui/components/index.d.ts.map +1 -0
  321. package/dist/tui/components/index.js +9 -0
  322. package/dist/tui/components/index.js.map +1 -0
  323. package/dist/tui/components/landing-screen.d.ts +12 -0
  324. package/dist/tui/components/landing-screen.d.ts.map +1 -0
  325. package/dist/tui/components/landing-screen.js +53 -0
  326. package/dist/tui/components/landing-screen.js.map +1 -0
  327. package/dist/tui/components/pixel-logo.d.ts +5 -0
  328. package/dist/tui/components/pixel-logo.d.ts.map +1 -0
  329. package/dist/tui/components/pixel-logo.js +27 -0
  330. package/dist/tui/components/pixel-logo.js.map +1 -0
  331. package/dist/tui/components/plan-utils.d.ts +11 -0
  332. package/dist/tui/components/plan-utils.d.ts.map +1 -0
  333. package/dist/tui/components/plan-utils.js +62 -0
  334. package/dist/tui/components/plan-utils.js.map +1 -0
  335. package/dist/tui/components/plans-list.d.ts +18 -0
  336. package/dist/tui/components/plans-list.d.ts.map +1 -0
  337. package/dist/tui/components/plans-list.js +46 -0
  338. package/dist/tui/components/plans-list.js.map +1 -0
  339. package/dist/tui/components/rich-footer.d.ts +29 -0
  340. package/dist/tui/components/rich-footer.d.ts.map +1 -0
  341. package/dist/tui/components/rich-footer.js +124 -0
  342. package/dist/tui/components/rich-footer.js.map +1 -0
  343. package/dist/tui/components/todo-panel.d.ts +14 -0
  344. package/dist/tui/components/todo-panel.d.ts.map +1 -0
  345. package/dist/tui/components/todo-panel.js +83 -0
  346. package/dist/tui/components/todo-panel.js.map +1 -0
  347. package/dist/tui/components.d.ts.map +1 -1
  348. package/dist/tui/components.js +14 -7
  349. package/dist/tui/components.js.map +1 -1
  350. package/dist/tui/index.d.ts +2 -1
  351. package/dist/tui/index.d.ts.map +1 -1
  352. package/dist/tui/index.js +99 -505
  353. package/dist/tui/index.js.map +1 -1
  354. package/dist/tui/overlays.d.ts +28 -0
  355. package/dist/tui/overlays.d.ts.map +1 -0
  356. package/dist/tui/overlays.js +154 -0
  357. package/dist/tui/overlays.js.map +1 -0
  358. package/dist/tui/themes.d.ts +5 -0
  359. package/dist/tui/themes.d.ts.map +1 -0
  360. package/dist/tui/themes.js +38 -0
  361. package/dist/tui/themes.js.map +1 -0
  362. package/dist/web/index.d.ts.map +1 -1
  363. package/dist/web/index.js +60 -122
  364. package/dist/web/index.js.map +1 -1
  365. package/dist/web/public/index.html +1 -1
  366. package/dist/web/public/messages.js +41 -25
  367. package/dist/web/public/state.js +72 -72
  368. package/dist/web/public/style.css +1126 -996
  369. package/dist/web/public/websocket.js +267 -267
  370. package/dist/web/session-bridge.d.ts.map +1 -1
  371. package/dist/web/session-bridge.js +82 -3
  372. package/dist/web/session-bridge.js.map +1 -1
  373. package/docs/ast-tools.md +117 -0
  374. package/package.json +60 -55
  375. package/scripts/check-line-count.mjs +53 -0
  376. package/scripts/copy-ast-wasm.mjs +32 -0
  377. package/src/agent/__tests__/session-restore.test.ts +284 -0
  378. package/src/agent/commands.ts +274 -0
  379. package/src/agent/compaction.ts +9 -2
  380. package/src/agent/index.ts +30 -25
  381. package/src/agent/memory.ts +60 -0
  382. package/src/agent/prompt-builder.ts +100 -26
  383. package/src/agent/providers/anthropic.ts +179 -179
  384. package/src/agent/providers/google.ts +210 -210
  385. package/src/agent/providers/ollama.ts +186 -186
  386. package/src/agent/providers/openai.ts +18 -3
  387. package/src/agent/session-chat.ts +480 -0
  388. package/src/agent/session-display.ts +195 -0
  389. package/src/agent/session-persistence.ts +146 -0
  390. package/src/agent/session-utils.ts +54 -0
  391. package/src/agent/session.ts +261 -718
  392. package/src/cli/index.ts +73 -4
  393. package/src/config/index.ts +342 -342
  394. package/src/index.ts +30 -3
  395. package/src/tools/__tests__/bash.test.ts +187 -1
  396. package/src/tools/__tests__/edit.test.ts +275 -11
  397. package/src/tools/__tests__/fetch.test.ts +1 -1
  398. package/src/tools/__tests__/glob.test.ts +1 -1
  399. package/src/tools/__tests__/grep.test.ts +1 -1
  400. package/src/tools/__tests__/ls.test.ts +1 -1
  401. package/src/tools/__tests__/read.test.ts +1 -1
  402. package/src/tools/__tests__/registry.test.ts +332 -0
  403. package/src/tools/__tests__/write_plan.test.ts +1 -1
  404. package/src/tools/codebase/ast/__tests__/integration.test.ts +452 -0
  405. package/src/tools/codebase/ast/__tests__/tree-sitter.test.ts +466 -0
  406. package/src/tools/codebase/ast/adapter.ts +44 -0
  407. package/src/tools/codebase/ast/tools.ts +322 -0
  408. package/src/tools/codebase/ast/tree-sitter-adapter.ts +468 -0
  409. package/src/tools/codebase/ast/tree-sitter-converters.ts +480 -0
  410. package/src/tools/codebase/ast/types.ts +102 -0
  411. package/src/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
  412. package/src/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
  413. package/src/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
  414. package/src/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
  415. package/src/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
  416. package/src/tools/{find.ts → codebase/find.ts} +4 -2
  417. package/src/tools/{glob.ts → codebase/glob.ts} +4 -2
  418. package/src/tools/{grep.ts → codebase/grep.ts} +4 -2
  419. package/src/tools/{ls.ts → codebase/ls.ts} +4 -2
  420. package/src/tools/{read.ts → codebase/read.ts} +5 -3
  421. package/src/tools/edit/diff-compute.ts +21 -0
  422. package/src/tools/edit/diff-render.ts +145 -0
  423. package/src/tools/edit/edit.ts +468 -0
  424. package/src/tools/edit/matcher.ts +70 -0
  425. package/src/tools/edit/parser.ts +131 -0
  426. package/src/tools/edit/types.ts +19 -0
  427. package/src/tools/{write_plan.ts → edit/write_plan.ts} +4 -2
  428. package/src/tools/execute/bash/constants.ts +93 -0
  429. package/src/tools/execute/bash/errors.ts +304 -0
  430. package/src/tools/execute/bash/execution.ts +211 -0
  431. package/src/tools/execute/bash/index.ts +131 -0
  432. package/src/tools/execute/bash/platform.ts +65 -0
  433. package/src/tools/execute/bash/security.ts +28 -0
  434. package/src/tools/execute/bash/validation.ts +78 -0
  435. package/src/tools/execute/git.ts +209 -0
  436. package/src/tools/execute/task_complete.ts +40 -0
  437. package/src/tools/install/clawhub.ts +222 -0
  438. package/src/tools/install/constants.ts +4 -0
  439. package/src/tools/install/install_skill.ts +150 -0
  440. package/src/tools/install/npm.ts +60 -0
  441. package/src/tools/install/skill-content.ts +108 -0
  442. package/src/tools/memory/delete_memory.ts +46 -0
  443. package/src/tools/memory/list_memory.ts +53 -0
  444. package/src/tools/memory/save_memory.ts +45 -0
  445. package/src/tools/registry.ts +119 -19
  446. package/src/tools/tree.ts +103 -0
  447. package/src/tools/types.ts +13 -0
  448. package/src/tools/{fetch.ts → web/fetch.ts} +4 -2
  449. package/src/tools/{search.ts → web/search.ts} +4 -2
  450. package/src/tui/commands.ts +135 -0
  451. package/src/tui/components/colors.ts +52 -0
  452. package/src/tui/components/header-bar.ts +88 -0
  453. package/src/tui/components/index.ts +8 -0
  454. package/src/tui/components/landing-screen.ts +67 -0
  455. package/src/tui/components/pixel-logo.ts +33 -0
  456. package/src/tui/components/plan-utils.ts +69 -0
  457. package/src/tui/components/plans-list.ts +55 -0
  458. package/src/tui/components/rich-footer.ts +146 -0
  459. package/src/tui/components/todo-panel.ts +95 -0
  460. package/src/tui/index.ts +335 -786
  461. package/src/tui/overlays.ts +203 -0
  462. package/src/tui/themes.ts +41 -0
  463. package/src/web/index.ts +64 -127
  464. package/src/web/public/index.html +1 -1
  465. package/src/web/public/messages.js +41 -25
  466. package/src/web/public/state.js +72 -72
  467. package/src/web/public/style.css +1126 -996
  468. package/src/web/public/websocket.js +267 -267
  469. package/src/web/session-bridge.ts +272 -193
  470. package/AGENT.md +0 -99
  471. package/src/tools/bash.ts +0 -125
  472. package/src/tools/edit.ts +0 -747
  473. package/src/tools/git.ts +0 -76
  474. package/src/tools/install_skill.ts +0 -539
  475. package/src/tui/components.ts +0 -635
@@ -1,261 +1,40 @@
1
- import { createHash } from 'node:crypto'
2
1
  import * as fs from 'node:fs'
3
- import * as os from 'node:os'
4
2
  import * as path from 'node:path'
5
3
  import type { Config } from '../config/index.js'
6
- import { saveTokenUsage } from '../config/tokens.js'
7
4
  import { FileReadTracker } from '../diff/apply.js'
8
- import { fmtErr } from '../tools/errors.js'
9
5
  import { ToolRegistry } from '../tools/registry.js'
10
- import type { ToolCall, ToolResult } from '../tools/types.js'
11
- import { compact, estimateMessagesTokens, shouldCompact } from './compaction.js'
12
- import { EventChannels, getGlobalEventBus } from './event-bus.js'
6
+ import type { ToolCall } from '../tools/types.js'
13
7
  import type { LLMMessage, LLMProvider } from './llm.js'
14
8
  import { buildSystemPrompt } from './prompt-builder.js'
15
9
  import { AnthropicProvider } from './providers/anthropic.js'
16
10
  import { GoogleProvider } from './providers/google.js'
17
11
  import { OllamaProvider } from './providers/ollama.js'
18
12
  import { OpenAIProvider } from './providers/openai.js'
19
-
20
- // ── Session persistence ────────────────────────────────────────────────────
21
-
22
- interface SessionData {
23
- cwd: string
24
- messages: LLMMessage[]
25
- totalInputTokens: number
26
- totalOutputTokens: number
27
- totalApiCalls: number
28
- totalCacheHitTokens?: number
29
- totalCacheMissTokens?: number
30
- mode: 'code' | 'plan' | 'ask'
31
- model: string
32
- provider: string
33
- updatedAt: string
34
- }
35
-
36
- function getSessionDir(): string {
37
- return path.join(os.homedir(), '.lonny', 'sessions')
38
- }
39
-
40
- function getSessionFilePath(cwd: string): string {
41
- const absPath = path.resolve(cwd)
42
- const hash = createHash('sha256').update(absPath, 'utf-8').digest('hex').slice(0, 12)
43
- const dirName = path.basename(absPath)
44
- const safeName = dirName.replace(/[<>:"/\\|?*]/g, '_')
45
- return path.join(getSessionDir(), `${safeName}-${hash}.json`)
46
- }
47
-
48
- function ensureDir(dir: string): void {
49
- if (!fs.existsSync(dir)) {
50
- fs.mkdirSync(dir, { recursive: true })
51
- }
52
- }
53
-
54
- // ── Colors ─────────────────────────────────────────────────────────────────
55
-
56
- const CY = '\x1b[36m'
57
- const GR = '\x1b[32m'
58
- const YE = '\x1b[33m'
59
- const RE = '\x1b[31m'
60
- const MG = '\x1b[35m'
61
- const GY = '\x1b[90m'
62
- const RS = '\x1b[0m'
63
- const BLD = '\x1b[1m'
64
- const TH = '\x1b[48;2;22;22;32m\x1b[38;2;150;150;170m' // dark bg + dim fg for thinking
65
-
66
- /** Get terminal width (columns), default to 80. */
67
- function termWidth(): number {
68
- return process.stdout.columns ?? 80
69
- }
70
-
71
- /** Visible width of a string (strip ANSI codes). ASCII=1, CJK/non-ASCII=2. */
72
- function visibleWidth(s: string): number {
73
- let w = 0
74
- for (let i = 0; i < s.length; i++) {
75
- if (s.charCodeAt(i) === 0x1b) {
76
- // Skip past escape sequence
77
- while (i < s.length && s[i] !== 'm') i++
78
- continue
79
- }
80
- w += s.charCodeAt(i) > 0x7e ? 2 : 1
81
- }
82
- return w
83
- }
84
-
85
- /** Visible prefix width for thinking box lines: " │" = 3 */
86
- const THINK_PREFIX_WIDTH = 3
87
-
88
- /** Build the top border of the thinking box */
89
- function thinkTopBorder(): string {
90
- return `\n ${GY}╭───────${RS}${TH} Think ${GY}────────────────────${RS}\n`
91
- }
92
-
93
- /** Build the bottom border of the thinking box */
94
- function thinkBottomBorder(): string {
95
- return ` ${GY}╰${'─'.repeat(42)}${RS}\n\n`
96
- }
13
+ import { runChat } from './session-chat.js'
14
+ import {
15
+ ensureDir,
16
+ findLegacySessionFile,
17
+ generateId,
18
+ getSessionDir,
19
+ getSessionFilePath,
20
+ getSessionFilesForCwd,
21
+ migrateSessionData,
22
+ type SessionData,
23
+ type SessionInfo,
24
+ } from './session-persistence.js'
25
+ import { sanitizeMessages } from './session-utils.js'
26
+
27
+ export { formatToolInput } from './session-display.js'
28
+
29
+ // ── Session output interface ────────────────────────────────────────────────
97
30
 
98
31
  export interface SessionOutput {
99
32
  write: (text: string) => void
100
- /** When true, tool invocation/result formatting is skipped (TUI handles it via event bus) */
101
33
  suppressToolOutput?: boolean
102
- /**
103
- * When autoApprove is false, called before dispatching write-type tool calls.
104
- * Return true to allow execution, false to reject. Tools are batched — one confirmation per turn.
105
- */
106
34
  confirmTool?: (toolCalls: ToolCall[]) => Promise<boolean>
107
35
  }
108
36
 
109
- function writeOut(text: string, output?: SessionOutput): void {
110
- if (output) {
111
- output.write(text)
112
- } else {
113
- process.stdout.write(text)
114
- }
115
- }
116
-
117
- function printUserMessage(prompt: string, output?: SessionOutput): void {
118
- // When suppressToolOutput is true (Web UI mode), the frontend already
119
- // displays the user message, so skip sending it as a chunk.
120
- if (output?.suppressToolOutput) return
121
- const line = ` ${GY}┃${RS} ${BLD}${CY}You${RS}`
122
- writeOut(`\n${line} ${prompt}\n\n`, output)
123
- }
124
-
125
- function printToolInvocation(tc: ToolCall, output?: SessionOutput): void {
126
- const detail = formatToolInput(tc)
127
- const isWrite = tc.name === 'write_plan' || tc.name === 'edit'
128
- const icon = isWrite ? `${YE}◆${RS}` : `${GR}◇${RS}`
129
- const label = isWrite ? `${YE}${tc.name}${RS}` : `${GR}${tc.name}${RS}`
130
- writeOut(`\n ${GY}│${RS} ${icon} ${label}${detail ? ` ${GY}${detail}${RS}` : ''}\n`, output)
131
- }
132
-
133
- function printToolResult(tc: ToolCall, result: ToolResult, output?: SessionOutput): void {
134
- if (!result.success) {
135
- writeOut(` ${GY}│${RS} ${RE}✖${RS} ${RE}${result.error}${RS}\n`, output)
136
- return
137
- }
138
- if (tc.name === 'read') {
139
- const fileCount = (result.output.match(/^=== /gm) || []).length
140
- writeOut(` ${GY}│${RS} ${GR}✔${RS} read ${fileCount} file(s)\n`, output)
141
- for (const line of result.output.split('\n')) {
142
- if (line.startsWith('=== ')) {
143
- const fp = line.slice(4, line.includes(' ===') ? line.indexOf(' ===') + 4 : undefined)
144
- writeOut(` ${GY}│${RS} ${GY}${fp}${RS}\n`, output)
145
- }
146
- }
147
- } else if (tc.name === 'glob') {
148
- const count = result.output.split('\n').filter(l => l && !l.startsWith('No')).length
149
- writeOut(` ${GY}│${RS} ${GR}✔${RS} glob ${count} match(es)\n`, output)
150
- } else if (tc.name === 'grep') {
151
- const count = result.output.split('\n').filter(l => l && !l.startsWith('No')).length
152
- writeOut(` ${GY}│${RS} ${GR}✔${RS} grep ${count} match(es)\n`, output)
153
- } else if (tc.name === 'bash') {
154
- const outLines = result.output.split('\n')
155
- const summary = outLines.length > 1 ? `(${outLines.length} lines)` : ''
156
- writeOut(` ${GY}│${RS} ${GR}✔${RS} bash ${summary}\n`, output)
157
- } else if (tc.name === 'edit') {
158
- writeOut(` ${GY}│${RS} ${GR}✔${RS} edit\n`, output)
159
- if (result.output) {
160
- for (const l of result.output.split('\n')) {
161
- if (l.trim()) writeOut(` ${GY}│${RS} ${l.trim()}\n`, output)
162
- }
163
- }
164
- } else if (tc.name === 'write_plan') {
165
- writeOut(` ${GY}│${RS} ${GR}✔${RS} ${result.output || tc.name}\n`, output)
166
- } else if (tc.name === 'search') {
167
- writeOut(
168
- ` ${GY}│${RS} ${GR}✔${RS} search: ${String(tc.input.query || '').slice(0, 80)}\n`,
169
- output,
170
- )
171
- } else {
172
- writeOut(` ${GY}│${RS} ${GR}✔${RS} ${tc.name}\n`, output)
173
- }
174
- }
175
-
176
- interface SingleEditShape {
177
- file_path: string
178
- old_string: string
179
- new_string: string
180
- }
181
-
182
- function isSingleEditShape(v: unknown): v is SingleEditShape {
183
- return (
184
- typeof v === 'object' &&
185
- v !== null &&
186
- 'file_path' in v &&
187
- 'old_string' in v &&
188
- 'new_string' in v
189
- )
190
- }
191
-
192
- export function formatToolInput(tc: ToolCall): string {
193
- const parts: string[] = []
194
- if (tc.name === 'read' && Array.isArray(tc.input.paths)) {
195
- parts.push(tc.input.paths.join(', '))
196
- } else if (tc.name === 'glob' && typeof tc.input.pattern === 'string') {
197
- parts.push(tc.input.pattern)
198
- } else if (tc.name === 'grep') {
199
- if (typeof tc.input.pattern === 'string') parts.push(`/${tc.input.pattern}/`)
200
- if (typeof tc.input.include === 'string') parts.push(`in:${tc.input.include}`)
201
- } else if (tc.name === 'ls') {
202
- parts.push(typeof tc.input.path === 'string' ? tc.input.path : '.')
203
- } else if (tc.name === 'bash') {
204
- const cmd = typeof tc.input.command === 'string' ? tc.input.command : ''
205
- parts.push(cmd.length > 80 ? `${cmd.slice(0, 80)}\u2026` : cmd)
206
- } else if (tc.name === 'search') {
207
- if (typeof tc.input.query === 'string') parts.push(tc.input.query.slice(0, 120))
208
- } else if (tc.name === 'write_plan') {
209
- if (typeof tc.input.filename === 'string') parts.push(tc.input.filename)
210
- } else if (tc.name === 'edit') {
211
- if (Array.isArray(tc.input.edits) && tc.input.edits.every(isSingleEditShape)) {
212
- const paths = tc.input.edits.map(e => e.file_path)
213
- parts.push(paths.join(', '))
214
- }
215
- }
216
- return parts.join(' \u2502 ')
217
- }
218
-
219
- function printTokenStats(
220
- turnIn: number,
221
- turnOut: number,
222
- totalIn: number,
223
- totalOut: number,
224
- turnApi: number,
225
- totalApi: number,
226
- output?: SessionOutput,
227
- turnCacheHit?: number,
228
- turnCacheMiss?: number,
229
- totalCacheHit?: number,
230
- totalCacheMiss?: number,
231
- ): void {
232
- const bus = getGlobalEventBus()
233
- bus.emit(EventChannels.TOKEN_STATS, {
234
- turnIn,
235
- turnOut,
236
- totalIn,
237
- totalOut,
238
- turnApi,
239
- totalApi,
240
- turnCacheHit,
241
- turnCacheMiss,
242
- totalCacheHit,
243
- totalCacheMiss,
244
- })
245
- // Skip terminal output in Web UI mode
246
- if (output?.suppressToolOutput) return
247
- const total = totalIn + totalOut
248
- let msg = ` ${GY}┃${RS} ${GY}${BLD}▴${RS}${GY}${turnIn}${RS} ${GY}${BLD}▾${RS}${GY}${turnOut}${RS} ${GY}total${RS} ${total} ${GY}calls${RS} ${turnApi}(${totalApi})`
249
- // Show cache hit rate if available
250
- const cacheHit = totalCacheHit ?? 0
251
- const cacheMiss = totalCacheMiss ?? 0
252
- const cacheTotal = cacheHit + cacheMiss
253
- if (cacheTotal > 0) {
254
- const pct = Math.round((cacheHit / cacheTotal) * 100)
255
- msg += ` ${GY}cached${RS} ${pct}%`
256
- }
257
- writeOut(`\n${msg}\n`, output)
258
- }
37
+ // ── Session class ────────────────────────────────────────────────────────────
259
38
 
260
39
  export class Session {
261
40
  messages: LLMMessage[]
@@ -265,12 +44,10 @@ export class Session {
265
44
  config: Config
266
45
  output?: SessionOutput
267
46
  private _onPlanWritten?: (display: string) => void
268
- /** Set the plan-written callback and propagate to ToolRegistry */
47
+
269
48
  set onPlanWritten(cb: ((display: string) => void) | undefined) {
270
49
  this._onPlanWritten = cb
271
- // Update context so setMode() picks it up too
272
50
  this.registry.updateContext({ onPlanWritten: cb })
273
- // Re-register write_plan tool with the new callback
274
51
  if (this.registry.has('write_plan')) {
275
52
  this.registry.reRegisterWritePlan(this.config.cwd, cb)
276
53
  }
@@ -288,8 +65,8 @@ export class Session {
288
65
  turnCacheMissTokens: number = 0
289
66
  totalCacheHitTokens: number = 0
290
67
  totalCacheMissTokens: number = 0
291
- private stopped: boolean = false
292
- private abortController: AbortController | null = null
68
+ stopped: boolean = false
69
+ abortController: AbortController | null = null
293
70
 
294
71
  constructor(config: Config, output?: SessionOutput) {
295
72
  this.config = config
@@ -321,25 +98,37 @@ export class Session {
321
98
  this.provider = new AnthropicProvider(config.apiKey, config.baseUrl, config.model)
322
99
  }
323
100
 
324
- // Placeholder until initSystemPrompt() is called
325
101
  this.messages = [{ role: 'system', content: '' }]
326
- // Initialize system prompt asynchronously
327
102
  this.initSystemPrompt(config)
328
103
  }
329
104
 
330
- /** Initialize the system prompt asynchronously */
331
105
  private initSystemPrompt(config: Config): void {
332
- buildSystemPrompt(config).then(prompt => {
333
- this.messages = [{ role: 'system', content: prompt }]
106
+ buildSystemPrompt(config, this.registry.getDefinitions()).then(prompt => {
107
+ if (this.messages.length <= 1) {
108
+ this.messages = [{ role: 'system', content: prompt }]
109
+ }
334
110
  })
335
111
  }
336
112
 
337
- /** Persist the current session to ~/.lonny/sessions/ */
113
+ sessionId: string = generateId()
114
+ sessionTitle: string = ''
115
+ sessionCreatedAt: string = new Date().toISOString()
116
+
338
117
  save(): void {
339
118
  const dir = getSessionDir()
340
119
  ensureDir(dir)
341
- const filePath = getSessionFilePath(this.config.cwd)
120
+ const filePath = getSessionFilePath(this.config.cwd, this.sessionId)
121
+ const now = new Date().toISOString()
122
+ let title = this.sessionTitle
123
+ if (!title) {
124
+ const firstUserMsg = this.messages.find(m => m.role === 'user')
125
+ if (firstUserMsg && typeof firstUserMsg.content === 'string') {
126
+ title = firstUserMsg.content.slice(0, 80).replace(/\n/g, ' ')
127
+ }
128
+ }
342
129
  const data: SessionData = {
130
+ id: this.sessionId,
131
+ title: title || undefined,
343
132
  cwd: path.resolve(this.config.cwd),
344
133
  messages: this.messages,
345
134
  totalInputTokens: this.totalInputTokens,
@@ -350,42 +139,105 @@ export class Session {
350
139
  mode: this.config.mode,
351
140
  model: this.config.model,
352
141
  provider: this.config.provider,
353
- updatedAt: new Date().toISOString(),
142
+ createdAt: this.sessionCreatedAt,
143
+ updatedAt: now,
354
144
  }
355
145
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
356
146
  }
357
147
 
358
- /** Try to load a saved session for the given cwd. Returns null if none exists. */
359
148
  static async load(config: Config, output?: SessionOutput): Promise<Session | null> {
360
- const filePath = getSessionFilePath(config.cwd)
361
- let data: SessionData
362
- try {
363
- data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as SessionData
364
- } catch {
365
- return null
149
+ const cwdSessions = getSessionFilesForCwd(config.cwd)
150
+ console.log(
151
+ `[session] Found ${cwdSessions.length} session files for cwd "${config.cwd}":`,
152
+ cwdSessions
153
+ .map(
154
+ f =>
155
+ `${f.fileName} (updatedAt=${f.data.updatedAt?.slice(0, 19)}, messages=${f.data.messages?.length || 0})`,
156
+ )
157
+ .join(', '),
158
+ )
159
+ if (cwdSessions.length > 0) {
160
+ cwdSessions.sort((a, b) => {
161
+ const aLen = a.data.messages?.length || 0
162
+ const bLen = b.data.messages?.length || 0
163
+ if (aLen !== bLen) return bLen - aLen
164
+ return b.data.updatedAt.localeCompare(a.data.updatedAt)
165
+ })
166
+ console.log(`[session] Loading session file: ${cwdSessions[0].fileName}`)
167
+ return Session.loadFromData(cwdSessions[0].data, config, output)
168
+ }
169
+
170
+ const legacyPath = findLegacySessionFile(config.cwd)
171
+ if (legacyPath) {
172
+ try {
173
+ const raw = JSON.parse(fs.readFileSync(legacyPath, 'utf-8')) as Record<string, unknown>
174
+ const data = migrateSessionData(raw)
175
+ const session = await Session.loadFromData(data, config, output)
176
+ session.save()
177
+ try {
178
+ fs.unlinkSync(legacyPath)
179
+ } catch {
180
+ /* ignore */
181
+ }
182
+ return session
183
+ } catch {
184
+ return null
185
+ }
366
186
  }
367
187
 
368
- // Verify the cwd matches (in case of hash collision or directory rename)
369
- const savedAbs = path.resolve(data.cwd)
370
- const currentAbs = path.resolve(config.cwd)
371
- if (savedAbs !== currentAbs) {
188
+ return null
189
+ }
190
+
191
+ static async loadById(
192
+ sessionId: string,
193
+ config: Config,
194
+ output?: SessionOutput,
195
+ ): Promise<Session | null> {
196
+ const allSessions = Session.listSessions()
197
+ const target = allSessions.find(s => s.id === sessionId)
198
+ if (!target) return null
199
+ const legacyPath = findLegacySessionFile(config.cwd)
200
+ if (legacyPath) {
201
+ try {
202
+ const raw = JSON.parse(fs.readFileSync(legacyPath, 'utf-8')) as Record<string, unknown>
203
+ const data = migrateSessionData(raw)
204
+ if (data.id === sessionId) {
205
+ return Session.loadFromData(data, config, output)
206
+ }
207
+ } catch {
208
+ /* ignore */
209
+ }
210
+ }
211
+ const filePath = getSessionFilePath(target.cwd, sessionId)
212
+ try {
213
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, unknown>
214
+ return Session.loadFromData(migrateSessionData(raw), config, output)
215
+ } catch {
372
216
  return null
373
217
  }
218
+ }
374
219
 
220
+ private static async loadFromData(
221
+ data: SessionData,
222
+ config: Config,
223
+ output?: SessionOutput,
224
+ ): Promise<Session> {
375
225
  const session = new Session(config, output)
376
- // Restore messages (replace the default system prompt with the saved one)
377
226
  session.messages = data.messages
378
- // Refresh the system prompt only if config actually changed (model, mode, etc.)
379
- // Compare the saved data vs current config to decide
380
- if (
381
- data.model !== config.model ||
382
- data.provider !== config.provider ||
383
- data.mode !== config.mode
384
- ) {
385
- const prompt = await buildSystemPrompt(config)
227
+ console.log(`[session] Loading session ${data.id}: ${data.messages.length} messages from disk`)
228
+ session.messages = sanitizeMessages(session.messages)
229
+ console.log(
230
+ `[session] After sanitize: ${session.messages.length} messages (removed ${data.messages.length - session.messages.length})`,
231
+ )
232
+ session.sessionId = data.id
233
+ session.sessionTitle = data.title || ''
234
+ session.sessionCreatedAt = data.createdAt
235
+ config.mode = data.mode
236
+ session.registry.setMode(data.mode)
237
+ if (data.model !== config.model || data.provider !== config.provider) {
238
+ const prompt = await buildSystemPrompt(config, session.registry.getDefinitions())
386
239
  session.messages[0] = { role: 'system', content: prompt }
387
240
  }
388
- // Restore token stats
389
241
  session.totalInputTokens = data.totalInputTokens
390
242
  session.totalOutputTokens = data.totalOutputTokens
391
243
  session.totalApiCalls = data.totalApiCalls
@@ -394,475 +246,166 @@ export class Session {
394
246
  return session
395
247
  }
396
248
 
397
- /** Remove the saved session file for the given cwd. */
398
249
  static clearSavedSession(cwd: string): void {
399
- const filePath = getSessionFilePath(cwd)
250
+ const cwdSessions = getSessionFilesForCwd(cwd)
251
+ for (const { fileName } of cwdSessions) {
252
+ try {
253
+ fs.unlinkSync(path.join(getSessionDir(), fileName))
254
+ } catch {
255
+ /* ignore */
256
+ }
257
+ }
258
+ const legacyPath = findLegacySessionFile(cwd)
259
+ if (legacyPath) {
260
+ try {
261
+ fs.unlinkSync(legacyPath)
262
+ } catch {
263
+ /* ignore */
264
+ }
265
+ }
266
+ }
267
+
268
+ static listSessions(maxCount?: number): SessionInfo[] {
269
+ const dir = getSessionDir()
270
+ try {
271
+ if (!fs.existsSync(dir)) return []
272
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'))
273
+ const sessions: SessionInfo[] = []
274
+ for (const fileName of files) {
275
+ try {
276
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, fileName), 'utf-8')) as Record<
277
+ string,
278
+ unknown
279
+ >
280
+ const data = migrateSessionData(raw)
281
+ const firstUserMsg = data.messages.find(m => m.role === 'user')
282
+ let title = data.title || ''
283
+ if (!title && firstUserMsg && typeof firstUserMsg.content === 'string') {
284
+ title = firstUserMsg.content.slice(0, 80).replace(/\n/g, ' ')
285
+ }
286
+ sessions.push({
287
+ id: data.id,
288
+ cwd: data.cwd,
289
+ title,
290
+ messageCount: data.messages.length,
291
+ mode: data.mode,
292
+ model: data.model,
293
+ provider: data.provider,
294
+ totalInputTokens: data.totalInputTokens,
295
+ totalOutputTokens: data.totalOutputTokens,
296
+ totalApiCalls: data.totalApiCalls,
297
+ createdAt: data.createdAt,
298
+ updatedAt: data.updatedAt,
299
+ fileName,
300
+ })
301
+ } catch {
302
+ // Skip corrupted files
303
+ }
304
+ }
305
+ sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
306
+ if (maxCount && maxCount > 0) {
307
+ return sessions.slice(0, maxCount)
308
+ }
309
+ return sessions
310
+ } catch {
311
+ return []
312
+ }
313
+ }
314
+
315
+ static deleteSession(id: string): boolean {
316
+ const sessions = Session.listSessions()
317
+ const target = sessions.find(s => s.id === id)
318
+ if (!target) return false
400
319
  try {
401
- fs.unlinkSync(filePath)
320
+ fs.unlinkSync(path.join(getSessionDir(), target.fileName))
321
+ return true
402
322
  } catch {
403
- // Ignore if file doesn't exist
323
+ return false
324
+ }
325
+ }
326
+
327
+ clearSavedSession(): void {
328
+ Session.clearSavedSession(this.config.cwd)
329
+ }
330
+
331
+ fork(): Session {
332
+ const forked = new Session(this.config, this.output)
333
+ forked.messages = [...this.messages]
334
+ forked.onPlanWritten = this.onPlanWritten
335
+ const baseTitle = this.sessionTitle || 'forked session'
336
+ forked.sessionTitle = `${baseTitle} (fork)`
337
+ forked.sessionId = generateId()
338
+ forked.sessionCreatedAt = new Date().toISOString()
339
+ forked.save()
340
+ return forked
341
+ }
342
+
343
+ exportSession(filePath?: string): string {
344
+ const dir = filePath || path.join(getSessionDir(), `export-${this.sessionId}.json`)
345
+ const now = new Date().toISOString()
346
+ let title = this.sessionTitle
347
+ if (!title) {
348
+ const firstUserMsg = this.messages.find(m => m.role === 'user')
349
+ if (firstUserMsg && typeof firstUserMsg.content === 'string') {
350
+ title = firstUserMsg.content.slice(0, 80).replace(/\n/g, ' ')
351
+ }
352
+ }
353
+ const exportData = {
354
+ id: this.sessionId,
355
+ title: title || '(untitled)',
356
+ cwd: path.resolve(this.config.cwd),
357
+ mode: this.config.mode,
358
+ model: this.config.model,
359
+ provider: this.config.provider,
360
+ totalInputTokens: this.totalInputTokens,
361
+ totalOutputTokens: this.totalOutputTokens,
362
+ totalApiCalls: this.totalApiCalls,
363
+ totalCacheHitTokens: this.totalCacheHitTokens,
364
+ totalCacheMissTokens: this.totalCacheMissTokens,
365
+ createdAt: this.sessionCreatedAt,
366
+ exportedAt: now,
367
+ messages: this.messages.map(m => ({
368
+ role: m.role,
369
+ content: typeof m.content === 'string' ? m.content : null,
370
+ tool_calls: m.tool_calls
371
+ ? m.tool_calls.map(tc => ({
372
+ id: tc.id,
373
+ name: tc.name,
374
+ input: tc.input,
375
+ }))
376
+ : undefined,
377
+ reasoning_content: m.reasoning_content,
378
+ })),
404
379
  }
380
+ const fullPath = path.resolve(dir)
381
+ ensureDir(path.dirname(fullPath))
382
+ fs.writeFileSync(fullPath, JSON.stringify(exportData, null, 2), 'utf-8')
383
+ return fullPath
405
384
  }
406
385
 
407
- async setMode(mode: 'code' | 'plan' | 'ask'): Promise<void> {
386
+ async setMode(mode: 'code' | 'plan' | 'ask' | 'loop'): Promise<void> {
408
387
  this.config.mode = mode
409
- const prompt = await buildSystemPrompt(this.config)
410
- this.messages[0] = { role: 'system', content: prompt }
411
388
  this.registry.setMode(mode)
389
+ const prompt = await buildSystemPrompt(this.config, this.registry.getDefinitions())
390
+ this.messages[0] = { role: 'system', content: prompt }
412
391
  this.save()
413
392
  }
414
393
 
415
- /** Stop the current conversation gracefully */
416
394
  stop(): void {
417
395
  this.stopped = true
418
- // Abort any in-flight LLM stream to stop token consumption immediately
419
396
  this.abortController?.abort()
420
397
  }
421
398
 
422
- /** Check if the session was stopped */
423
399
  isStopped(): boolean {
424
400
  return this.stopped
425
401
  }
426
402
 
427
- /** Reset the stopped flag for a new conversation */
428
403
  resetStopped(): void {
429
404
  this.stopped = false
430
- // Create new AbortController for next conversation
431
405
  this.abortController = new AbortController()
432
406
  }
433
407
 
434
408
  async chat(userPrompt: string): Promise<void> {
435
- const bus = getGlobalEventBus()
436
- const out = this.output
437
- printUserMessage(userPrompt, out)
438
- this.messages.push({ role: 'user', content: userPrompt })
439
- // Save immediately after user message so page refresh doesn't lose it
440
- this.save()
441
-
442
- // Reset per-turn counters
443
- this.turnInputTokens = 0
444
- this.turnOutputTokens = 0
445
- this.turnApiCalls = 0
446
- this.turnCacheHitTokens = 0
447
- this.turnCacheMissTokens = 0
448
-
449
- let iterations = 0
450
- const maxIterations = 30
451
-
452
- // Reset stopped flag for new conversation
453
- this.resetStopped()
454
- // Create a new AbortController for this chat invocation
455
- // (a new controller is needed each time because abort() is one-shot)
456
- this.abortController = new AbortController()
457
-
458
- // Declare toolCalls outside the loop so we can reference it in stop check
459
- let toolCalls: ToolCall[] = []
460
-
461
- while (iterations < maxIterations) {
462
- // Check if stop was requested
463
- if (this.isStopped()) {
464
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
465
- this.save()
466
- return
467
- }
468
- iterations++
469
- this.turnApiCalls++
470
- this.totalApiCalls++
471
- toolCalls = []
472
- let fullResponse = ''
473
- let reasoningContent: string | undefined
474
- let reasoningOutput = false
475
- let reasoningLineStart = false
476
-
477
- bus.emit(EventChannels.TURN_START, { prompt: userPrompt, iteration: iterations })
478
- bus.emit(EventChannels.LLM_STREAM_START, { iteration: iterations })
479
-
480
- const stream = this.provider.chat(
481
- this.messages,
482
- this.registry.getDefinitions(),
483
- this.abortController.signal,
484
- )
485
-
486
- try {
487
- for await (const chunk of stream) {
488
- if (chunk.reasoning_content) {
489
- reasoningContent = chunk.reasoning_content
490
- // Stream reasoning content in real-time (only when no text in same chunk)
491
- if (!chunk.text) {
492
- // Emit thinking via EventBus for Web UI
493
- bus.emit(EventChannels.THINKING, { text: chunk.reasoning_content })
494
- if (!reasoningOutput) {
495
- reasoningOutput = true
496
- reasoningLineStart = true
497
- if (!out?.suppressToolOutput) {
498
- writeOut(thinkTopBorder(), out)
499
- }
500
- }
501
- // Terminal display with box drawing (skip in Web UI mode, handled by EventBus)
502
- if (!out?.suppressToolOutput) {
503
- // Track column position on current line for wrapping
504
- let thinkCol = 0
505
- // Handle newlines in streamed content - add left border on each new line
506
- // Also manually wrap long lines so wrapped lines keep the │ prefix.
507
- let remaining = chunk.reasoning_content
508
- const maxContentWidth = termWidth() - THINK_PREFIX_WIDTH
509
- while (remaining.length > 0) {
510
- if (reasoningLineStart) {
511
- writeOut(` ${GY}│${RS}${TH}`, out)
512
- reasoningLineStart = false
513
- thinkCol = 0
514
- }
515
- const nlIdx = remaining.indexOf('\n')
516
- if (nlIdx === -1) {
517
- // No newline — write as much as fits on current line, wrap if needed
518
- while (remaining.length > 0) {
519
- const segWidth = visibleWidth(remaining)
520
- const avail = maxContentWidth - thinkCol
521
- if (segWidth <= avail) {
522
- // Fits entirely on current line
523
- writeOut(remaining, out)
524
- thinkCol += segWidth
525
- remaining = ''
526
- } else if (avail <= 0) {
527
- // Current line is full, wrap to next
528
- writeOut(`${RS}\n`, out)
529
- writeOut(` ${GY}│${RS}${TH}`, out)
530
- thinkCol = 0
531
- } else {
532
- // Write first part that fits, then wrap
533
- // Find character boundary that fits within avail
534
- let cut = avail
535
- while (cut > 0 && visibleWidth(remaining.slice(0, cut)) > avail) cut--
536
- if (cut <= 0) cut = 1
537
- writeOut(remaining.slice(0, cut), out)
538
- writeOut(`${RS}\n`, out)
539
- writeOut(` ${GY}│${RS}${TH}`, out)
540
- thinkCol = 0
541
- remaining = remaining.slice(cut)
542
- }
543
- }
544
- } else {
545
- // Has newline — process the segment up to newline
546
- const segment = remaining.slice(0, nlIdx)
547
- const segWidth = visibleWidth(segment)
548
- const avail = maxContentWidth - thinkCol
549
- if (segWidth <= avail) {
550
- // Segment fits on current line
551
- writeOut(segment, out)
552
- writeOut(`${RS}\n`, out)
553
- reasoningLineStart = true
554
- thinkCol = 0
555
- } else {
556
- // Segment too long — write what fits, wrap, then rest
557
- let rest = segment
558
- // Write remainder of current line
559
- if (avail > 0) {
560
- let cut = avail
561
- while (cut > 0 && visibleWidth(rest.slice(0, cut)) > avail) cut--
562
- if (cut <= 0) cut = 1
563
- writeOut(rest.slice(0, cut), out)
564
- rest = rest.slice(cut)
565
- }
566
- writeOut(`${RS}\n`, out)
567
- reasoningLineStart = true
568
- thinkCol = 0
569
- // Write rest of segment on continuation line(s)
570
- if (rest.length > 0) {
571
- writeOut(` ${GY}│${RS}${TH}`, out)
572
- reasoningLineStart = false
573
- while (rest.length > 0) {
574
- const rw = visibleWidth(rest)
575
- if (rw <= maxContentWidth) {
576
- writeOut(rest, out)
577
- thinkCol = rw
578
- rest = ''
579
- } else {
580
- let cut = maxContentWidth
581
- while (cut > 0 && visibleWidth(rest.slice(0, cut)) > maxContentWidth)
582
- cut--
583
- if (cut <= 0) cut = 1
584
- writeOut(rest.slice(0, cut), out)
585
- writeOut(`${RS}\n`, out)
586
- writeOut(` ${GY}│${RS}${TH}`, out)
587
- rest = rest.slice(cut)
588
- }
589
- }
590
- }
591
- writeOut(`${RS}\n`, out)
592
- reasoningLineStart = true
593
- thinkCol = 0
594
- }
595
- remaining = remaining.slice(nlIdx + 1)
596
- }
597
- }
598
- }
599
- }
600
- }
601
- if (chunk.type === 'text' && chunk.text) {
602
- if (reasoningOutput) {
603
- bus.emit(EventChannels.THINKING_END, {})
604
- if (!out?.suppressToolOutput) {
605
- writeOut(`${RS}\n`, out)
606
- writeOut(thinkBottomBorder(), out)
607
- }
608
- reasoningOutput = false
609
- reasoningLineStart = false
610
- }
611
- fullResponse += chunk.text
612
- writeOut(chunk.text, out)
613
- } else if (chunk.type === 'tool_use' && chunk.tool_call) {
614
- toolCalls.push(chunk.tool_call)
615
- } else if (chunk.type === 'complete') {
616
- if (chunk.usage) {
617
- this.turnInputTokens += chunk.usage.input_tokens
618
- this.turnOutputTokens += chunk.usage.output_tokens
619
- this.totalInputTokens += chunk.usage.input_tokens
620
- this.totalOutputTokens += chunk.usage.output_tokens
621
- // Accumulate DeepSeek cache hit/miss tokens
622
- if (chunk.usage.prompt_cache_hit_tokens != null) {
623
- this.turnCacheHitTokens += chunk.usage.prompt_cache_hit_tokens
624
- this.totalCacheHitTokens += chunk.usage.prompt_cache_hit_tokens
625
- }
626
- if (chunk.usage.prompt_cache_miss_tokens != null) {
627
- this.turnCacheMissTokens += chunk.usage.prompt_cache_miss_tokens
628
- this.totalCacheMissTokens += chunk.usage.prompt_cache_miss_tokens
629
- }
630
- }
631
- if (chunk.finish_reason === 'stop' || chunk.finish_reason === 'end_turn') {
632
- if (toolCalls.length === 0) {
633
- const finalAssistantMsg: LLMMessage = {
634
- role: 'assistant',
635
- content: fullResponse || null,
636
- reasoning_content: reasoningContent,
637
- }
638
- this.messages.push(finalAssistantMsg)
639
- printTokenStats(
640
- this.turnInputTokens,
641
- this.turnOutputTokens,
642
- this.totalInputTokens,
643
- this.totalOutputTokens,
644
- this.turnApiCalls,
645
- this.totalApiCalls,
646
- out,
647
- this.turnCacheHitTokens,
648
- this.turnCacheMissTokens,
649
- this.totalCacheHitTokens,
650
- this.totalCacheMissTokens,
651
- )
652
- writeOut('\n\n', out)
653
- saveTokenUsage(
654
- this.config.cwd,
655
- this.turnInputTokens,
656
- this.turnOutputTokens,
657
- this.turnApiCalls,
658
- )
659
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 })
660
- this.save()
661
- return
662
- }
663
- }
664
- }
665
- }
666
- } catch (e) {
667
- const errMsg = fmtErr(e)
668
- const partialContent = fullResponse ? fullResponse.slice(0, 500) : '(empty)'
669
- if (!out?.suppressToolOutput) {
670
- writeOut(`\n${RE}Stream error:${RS} ${errMsg}`, out)
671
- writeOut(`\n ${GY}┃${RS} Partial response: ${partialContent}\n`, out)
672
- }
673
- console.error('[session] Stream error:', errMsg, '| Partial response:', partialContent)
674
- if (reasoningOutput) {
675
- bus.emit(EventChannels.THINKING_END, {})
676
- if (!out?.suppressToolOutput) {
677
- writeOut(`${RS}\n`, out)
678
- writeOut(thinkBottomBorder(), out)
679
- }
680
- }
681
- bus.emit(EventChannels.LLM_STREAM_END, { iteration: iterations, toolCallCount: 0 })
682
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 })
683
-
684
- // If there are pending toolCalls that weren't executed due to abort,
685
- // add an assistant message so the model knows they weren't executed
686
- if (toolCalls.length > 0) {
687
- const interruptedMsg: LLMMessage = {
688
- role: 'assistant',
689
- content: null,
690
- tool_calls: toolCalls,
691
- reasoning_content: reasoningContent,
692
- }
693
- this.messages.push(interruptedMsg)
694
- }
695
-
696
- saveTokenUsage(
697
- this.config.cwd,
698
- this.turnInputTokens,
699
- this.turnOutputTokens,
700
- this.turnApiCalls,
701
- )
702
- this.save()
703
- return
704
- }
705
-
706
- bus.emit(EventChannels.LLM_STREAM_END, {
707
- iteration: iterations,
708
- toolCallCount: toolCalls.length,
709
- })
710
-
711
- // Close reasoning display if still open (model ended with tool calls, no text)
712
- if (reasoningOutput) {
713
- bus.emit(EventChannels.THINKING_END, {})
714
- if (!out?.suppressToolOutput) {
715
- writeOut(`${RS}\n`, out)
716
- writeOut(thinkBottomBorder(), out)
717
- }
718
- reasoningOutput = false
719
- reasoningLineStart = false
720
- }
721
-
722
- if (toolCalls.length === 0) {
723
- if (fullResponse) {
724
- const finalAssistantMsg: LLMMessage = {
725
- role: 'assistant',
726
- content: fullResponse,
727
- reasoning_content: reasoningContent,
728
- }
729
- this.messages.push(finalAssistantMsg)
730
- printTokenStats(
731
- this.turnInputTokens,
732
- this.turnOutputTokens,
733
- this.totalInputTokens,
734
- this.totalOutputTokens,
735
- this.turnApiCalls,
736
- this.totalApiCalls,
737
- out,
738
- this.turnCacheHitTokens,
739
- this.turnCacheMissTokens,
740
- this.totalCacheHitTokens,
741
- this.totalCacheMissTokens,
742
- )
743
- writeOut('\n\n', out)
744
- }
745
- saveTokenUsage(
746
- this.config.cwd,
747
- this.turnInputTokens,
748
- this.turnOutputTokens,
749
- this.turnApiCalls,
750
- )
751
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: 0 })
752
- this.save()
753
- return
754
- }
755
-
756
- const assistantMsg: LLMMessage = {
757
- role: 'assistant',
758
- content: fullResponse || null,
759
- tool_calls: toolCalls,
760
- reasoning_content: reasoningContent,
761
- }
762
- this.messages.push(assistantMsg)
763
- this.save()
764
-
765
- // ── User confirmation for write-type tool calls ──
766
- if (!this.config.autoApprove && this.output?.confirmTool && toolCalls.length > 0) {
767
- const writeTools = ['edit', 'bash', 'write_plan', 'install_skill']
768
- const needsConfirm = toolCalls.filter(tc => writeTools.includes(tc.name))
769
- if (needsConfirm.length > 0) {
770
- const approved = await this.output.confirmTool(needsConfirm)
771
- if (!approved) {
772
- const rejectMsg: LLMMessage = {
773
- role: 'tool',
774
- content:
775
- 'USER_REJECTED: The user declined to execute the requested tool calls. Try a different approach.',
776
- tool_call_id: needsConfirm[0].id,
777
- name: 'user_feedback',
778
- }
779
- this.messages.push(rejectMsg)
780
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
781
- continue
782
- }
783
- }
784
- }
785
-
786
- for (let i = 0; i < toolCalls.length; i++) {
787
- const tc = toolCalls[i]
788
- // Check if stop was requested after each tool call
789
- if (this.isStopped()) {
790
- // Add assistant message with remaining tool_calls so model knows they weren't executed
791
- const remainingToolCalls = toolCalls.slice(i)
792
- if (remainingToolCalls.length > 0) {
793
- const interruptedMsg: LLMMessage = {
794
- role: 'assistant',
795
- content: null,
796
- tool_calls: remainingToolCalls,
797
- }
798
- this.messages.push(interruptedMsg)
799
- }
800
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
801
- this.save()
802
- return
803
- }
804
- bus.emit(EventChannels.TOOL_CALL, { name: tc.name, input: tc.input, id: tc.id })
805
- if (!out?.suppressToolOutput) {
806
- printToolInvocation(tc, out)
807
- }
808
- const result: ToolResult = await this.registry.dispatch(tc)
809
- if (result.success) {
810
- bus.emit(EventChannels.TOOL_RESULT, { name: tc.name, id: tc.id, output: result.output })
811
- } else {
812
- bus.emit(EventChannels.TOOL_ERROR, { name: tc.name, id: tc.id, error: result.error })
813
- }
814
- if (!out?.suppressToolOutput) {
815
- printToolResult(tc, result, out)
816
- }
817
-
818
- const resultMsg: LLMMessage = {
819
- role: 'tool',
820
- content: result.success ? result.output : `ERROR: ${result.error}`,
821
- tool_call_id: tc.id,
822
- name: tc.name,
823
- }
824
- this.messages.push(resultMsg)
825
- }
826
-
827
- this.save()
828
-
829
- // End the current turn before next iteration — frontend creates a new message
830
- bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
831
-
832
- // Check if compaction is needed
833
- if (shouldCompact(this.messages, this.config.contextWindow)) {
834
- const before = this.messages.length
835
- const result = compact(this.messages, this.config.contextWindow)
836
- if (result.compressed) {
837
- this.messages = result.messages
838
- bus.emit(EventChannels.COMPACTION_TRIGGERED, { before, after: result.newCount })
839
- if (out && !out.suppressToolOutput) {
840
- out.write(
841
- `\n ${GY}┃${RS} ${GY}📦 Compressed context: ${before} → ${result.newCount} messages${RS}\n`,
842
- )
843
- }
844
- }
845
- }
846
- }
847
-
848
- if (iterations >= maxIterations) {
849
- printTokenStats(
850
- this.turnInputTokens,
851
- this.turnOutputTokens,
852
- this.totalInputTokens,
853
- this.totalOutputTokens,
854
- this.turnApiCalls,
855
- this.totalApiCalls,
856
- out,
857
- this.turnCacheHitTokens,
858
- this.turnCacheMissTokens,
859
- this.totalCacheHitTokens,
860
- this.totalCacheMissTokens,
861
- )
862
- writeOut('\nAgent reached maximum iterations. Stopping.\n', out)
863
- }
864
-
865
- saveTokenUsage(this.config.cwd, this.turnInputTokens, this.turnOutputTokens, this.turnApiCalls)
866
- this.save()
409
+ return runChat(this, userPrompt)
867
410
  }
868
411
  }