@prestyj/cli 4.3.210 → 4.3.237

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 (889) hide show
  1. package/README.md +17 -9
  2. package/dist/cli/auth.d.ts +4 -0
  3. package/dist/cli/auth.d.ts.map +1 -0
  4. package/dist/cli/auth.js +344 -0
  5. package/dist/cli/auth.js.map +1 -0
  6. package/dist/cli/command-routing.d.ts +28 -0
  7. package/dist/cli/command-routing.d.ts.map +1 -0
  8. package/dist/cli/command-routing.js +53 -0
  9. package/dist/cli/command-routing.js.map +1 -0
  10. package/dist/cli/command-routing.test.d.ts +2 -0
  11. package/dist/cli/command-routing.test.d.ts.map +1 -0
  12. package/dist/cli/command-routing.test.js +60 -0
  13. package/dist/cli/command-routing.test.js.map +1 -0
  14. package/dist/cli/mcp.d.ts +2 -0
  15. package/dist/cli/mcp.d.ts.map +1 -0
  16. package/dist/cli/mcp.js +309 -0
  17. package/dist/cli/mcp.js.map +1 -0
  18. package/dist/cli/pixel.d.ts +27 -0
  19. package/dist/cli/pixel.d.ts.map +1 -0
  20. package/dist/cli/pixel.js +103 -0
  21. package/dist/cli/pixel.js.map +1 -0
  22. package/dist/cli/shared.d.ts +13 -0
  23. package/dist/cli/shared.d.ts.map +1 -0
  24. package/dist/cli/shared.js +82 -0
  25. package/dist/cli/shared.js.map +1 -0
  26. package/dist/cli.d.ts +4 -1
  27. package/dist/cli.d.ts.map +1 -1
  28. package/dist/cli.js +174 -732
  29. package/dist/cli.js.map +1 -1
  30. package/dist/config.d.ts +4 -1
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/config.js +14 -1
  33. package/dist/config.js.map +1 -1
  34. package/dist/config.test.d.ts +2 -0
  35. package/dist/config.test.d.ts.map +1 -0
  36. package/dist/config.test.js +29 -0
  37. package/dist/config.test.js.map +1 -0
  38. package/dist/core/agent-session-compaction.test.d.ts +2 -0
  39. package/dist/core/agent-session-compaction.test.d.ts.map +1 -0
  40. package/dist/core/agent-session-compaction.test.js +114 -0
  41. package/dist/core/agent-session-compaction.test.js.map +1 -0
  42. package/dist/core/agent-session.d.ts +0 -23
  43. package/dist/core/agent-session.d.ts.map +1 -1
  44. package/dist/core/agent-session.js +6 -101
  45. package/dist/core/agent-session.js.map +1 -1
  46. package/dist/core/checkpoint-store.d.ts +93 -0
  47. package/dist/core/checkpoint-store.d.ts.map +1 -0
  48. package/dist/core/checkpoint-store.js +188 -0
  49. package/dist/core/checkpoint-store.js.map +1 -0
  50. package/dist/core/checkpoint-store.test.d.ts +2 -0
  51. package/dist/core/checkpoint-store.test.d.ts.map +1 -0
  52. package/dist/core/checkpoint-store.test.js +111 -0
  53. package/dist/core/checkpoint-store.test.js.map +1 -0
  54. package/dist/core/compaction/compactor.d.ts +4 -0
  55. package/dist/core/compaction/compactor.d.ts.map +1 -1
  56. package/dist/core/compaction/compactor.js +81 -28
  57. package/dist/core/compaction/compactor.js.map +1 -1
  58. package/dist/core/compaction/compactor.test.js +33 -9
  59. package/dist/core/compaction/compactor.test.js.map +1 -1
  60. package/dist/core/compaction/token-estimator.test.js +1 -1
  61. package/dist/core/continue-replay-inventory.test.d.ts +2 -0
  62. package/dist/core/continue-replay-inventory.test.d.ts.map +1 -0
  63. package/dist/core/continue-replay-inventory.test.js +42 -0
  64. package/dist/core/continue-replay-inventory.test.js.map +1 -0
  65. package/dist/core/goal-controller.d.ts +4 -0
  66. package/dist/core/goal-controller.d.ts.map +1 -1
  67. package/dist/core/goal-controller.js +223 -32
  68. package/dist/core/goal-controller.js.map +1 -1
  69. package/dist/core/goal-controller.test.js +387 -22
  70. package/dist/core/goal-controller.test.js.map +1 -1
  71. package/dist/core/goal-lifecycle-smoke.test.js +49 -8
  72. package/dist/core/goal-lifecycle-smoke.test.js.map +1 -1
  73. package/dist/core/goal-prerequisites.d.ts +23 -0
  74. package/dist/core/goal-prerequisites.d.ts.map +1 -0
  75. package/dist/core/goal-prerequisites.js +114 -0
  76. package/dist/core/goal-prerequisites.js.map +1 -0
  77. package/dist/core/goal-prerequisites.test.d.ts +2 -0
  78. package/dist/core/goal-prerequisites.test.d.ts.map +1 -0
  79. package/dist/core/goal-prerequisites.test.js +118 -0
  80. package/dist/core/goal-prerequisites.test.js.map +1 -0
  81. package/dist/core/goal-store.d.ts +12 -1
  82. package/dist/core/goal-store.d.ts.map +1 -1
  83. package/dist/core/goal-store.js +146 -40
  84. package/dist/core/goal-store.js.map +1 -1
  85. package/dist/core/goal-store.test.js +65 -8
  86. package/dist/core/goal-store.test.js.map +1 -1
  87. package/dist/core/goal-verifier.d.ts.map +1 -1
  88. package/dist/core/goal-verifier.js +4 -1
  89. package/dist/core/goal-verifier.js.map +1 -1
  90. package/dist/core/goal-verifier.test.js +43 -0
  91. package/dist/core/goal-verifier.test.js.map +1 -1
  92. package/dist/core/goal-worker.d.ts +2 -0
  93. package/dist/core/goal-worker.d.ts.map +1 -1
  94. package/dist/core/goal-worker.js +31 -7
  95. package/dist/core/goal-worker.js.map +1 -1
  96. package/dist/core/goal-worker.test.js +44 -0
  97. package/dist/core/goal-worker.test.js.map +1 -1
  98. package/dist/core/ideal-review.d.ts +20 -0
  99. package/dist/core/ideal-review.d.ts.map +1 -0
  100. package/dist/core/ideal-review.js +55 -0
  101. package/dist/core/ideal-review.js.map +1 -0
  102. package/dist/core/ideal-review.test.d.ts +2 -0
  103. package/dist/core/ideal-review.test.d.ts.map +1 -0
  104. package/dist/core/ideal-review.test.js +59 -0
  105. package/dist/core/ideal-review.test.js.map +1 -0
  106. package/dist/core/index.d.ts +1 -0
  107. package/dist/core/index.d.ts.map +1 -1
  108. package/dist/core/index.js +1 -0
  109. package/dist/core/index.js.map +1 -1
  110. package/dist/core/loop-breaker.d.ts +46 -0
  111. package/dist/core/loop-breaker.d.ts.map +1 -0
  112. package/dist/core/loop-breaker.js +79 -0
  113. package/dist/core/loop-breaker.js.map +1 -0
  114. package/dist/core/loop-breaker.test.d.ts +2 -0
  115. package/dist/core/loop-breaker.test.d.ts.map +1 -0
  116. package/dist/core/loop-breaker.test.js +110 -0
  117. package/dist/core/loop-breaker.test.js.map +1 -0
  118. package/dist/core/mcp/client.d.ts +20 -0
  119. package/dist/core/mcp/client.d.ts.map +1 -1
  120. package/dist/core/mcp/client.js +55 -11
  121. package/dist/core/mcp/client.js.map +1 -1
  122. package/dist/core/mcp/defaults.d.ts +7 -0
  123. package/dist/core/mcp/defaults.d.ts.map +1 -1
  124. package/dist/core/mcp/defaults.js +14 -0
  125. package/dist/core/mcp/defaults.js.map +1 -1
  126. package/dist/core/mcp/index.d.ts +6 -1
  127. package/dist/core/mcp/index.d.ts.map +1 -1
  128. package/dist/core/mcp/index.js +3 -1
  129. package/dist/core/mcp/index.js.map +1 -1
  130. package/dist/core/mcp/parse-add-command.d.ts +25 -0
  131. package/dist/core/mcp/parse-add-command.d.ts.map +1 -0
  132. package/dist/core/mcp/parse-add-command.js +220 -0
  133. package/dist/core/mcp/parse-add-command.js.map +1 -0
  134. package/dist/core/mcp/parse-add-command.test.d.ts +2 -0
  135. package/dist/core/mcp/parse-add-command.test.d.ts.map +1 -0
  136. package/dist/core/mcp/parse-add-command.test.js +75 -0
  137. package/dist/core/mcp/parse-add-command.test.js.map +1 -0
  138. package/dist/core/mcp/store.d.ts +80 -0
  139. package/dist/core/mcp/store.d.ts.map +1 -0
  140. package/dist/core/mcp/store.js +157 -0
  141. package/dist/core/mcp/store.js.map +1 -0
  142. package/dist/core/mcp/store.test.d.ts +2 -0
  143. package/dist/core/mcp/store.test.d.ts.map +1 -0
  144. package/dist/core/mcp/store.test.js +101 -0
  145. package/dist/core/mcp/store.test.js.map +1 -0
  146. package/dist/core/model-registry.d.ts +2 -2
  147. package/dist/core/model-registry.js +4 -4
  148. package/dist/core/model-registry.js.map +1 -1
  149. package/dist/core/model-registry.test.js +1 -1
  150. package/dist/core/model-registry.test.js.map +1 -1
  151. package/dist/core/prompt-commands.d.ts.map +1 -1
  152. package/dist/core/prompt-commands.js +64 -894
  153. package/dist/core/prompt-commands.js.map +1 -1
  154. package/dist/core/prompt-commands.test.js +49 -41
  155. package/dist/core/prompt-commands.test.js.map +1 -1
  156. package/dist/core/regrounding.d.ts +23 -0
  157. package/dist/core/regrounding.d.ts.map +1 -0
  158. package/dist/core/regrounding.js +21 -0
  159. package/dist/core/regrounding.js.map +1 -0
  160. package/dist/core/regrounding.test.d.ts +2 -0
  161. package/dist/core/regrounding.test.d.ts.map +1 -0
  162. package/dist/core/regrounding.test.js +38 -0
  163. package/dist/core/regrounding.test.js.map +1 -0
  164. package/dist/core/runtime-mode.d.ts +23 -0
  165. package/dist/core/runtime-mode.d.ts.map +1 -0
  166. package/dist/core/runtime-mode.js +30 -0
  167. package/dist/core/runtime-mode.js.map +1 -0
  168. package/dist/core/session-manager.d.ts +4 -0
  169. package/dist/core/session-manager.d.ts.map +1 -1
  170. package/dist/core/session-manager.js +20 -0
  171. package/dist/core/session-manager.js.map +1 -1
  172. package/dist/core/session-restore-display.test.d.ts +2 -0
  173. package/dist/core/session-restore-display.test.d.ts.map +1 -0
  174. package/dist/core/session-restore-display.test.js +78 -0
  175. package/dist/core/session-restore-display.test.js.map +1 -0
  176. package/dist/core/settings-manager.d.ts +4 -2
  177. package/dist/core/settings-manager.d.ts.map +1 -1
  178. package/dist/core/settings-manager.js +3 -6
  179. package/dist/core/settings-manager.js.map +1 -1
  180. package/dist/core/slash-commands.d.ts +0 -2
  181. package/dist/core/slash-commands.d.ts.map +1 -1
  182. package/dist/core/slash-commands.js +10 -15
  183. package/dist/core/slash-commands.js.map +1 -1
  184. package/dist/core/style-packs/index.js +1 -1
  185. package/dist/core/style-packs/index.js.map +1 -1
  186. package/dist/core/task-store.d.ts +26 -0
  187. package/dist/core/task-store.d.ts.map +1 -0
  188. package/dist/core/task-store.js +147 -0
  189. package/dist/core/task-store.js.map +1 -0
  190. package/dist/core/tasks-store.d.ts +24 -0
  191. package/dist/core/tasks-store.d.ts.map +1 -0
  192. package/dist/core/tasks-store.js +81 -0
  193. package/dist/core/tasks-store.js.map +1 -0
  194. package/dist/core/thinking-level.d.ts +5 -0
  195. package/dist/core/thinking-level.d.ts.map +1 -0
  196. package/dist/core/thinking-level.js +59 -0
  197. package/dist/core/thinking-level.js.map +1 -0
  198. package/dist/core/thinking-level.test.d.ts +2 -0
  199. package/dist/core/thinking-level.test.d.ts.map +1 -0
  200. package/dist/core/thinking-level.test.js +38 -0
  201. package/dist/core/thinking-level.test.js.map +1 -0
  202. package/dist/core/verify-commands.js +4 -4
  203. package/dist/core/verify-commands.js.map +1 -1
  204. package/dist/index.d.ts +1 -1
  205. package/dist/index.d.ts.map +1 -1
  206. package/dist/index.js +1 -1
  207. package/dist/index.js.map +1 -1
  208. package/dist/interactive.d.ts.map +1 -1
  209. package/dist/interactive.js +3 -1
  210. package/dist/interactive.js.map +1 -1
  211. package/dist/system-prompt.d.ts +5 -1
  212. package/dist/system-prompt.d.ts.map +1 -1
  213. package/dist/system-prompt.js +76 -48
  214. package/dist/system-prompt.js.map +1 -1
  215. package/dist/system-prompt.test.js +98 -46
  216. package/dist/system-prompt.test.js.map +1 -1
  217. package/dist/tools/bash.d.ts +6 -1
  218. package/dist/tools/bash.d.ts.map +1 -1
  219. package/dist/tools/bash.js +21 -3
  220. package/dist/tools/bash.js.map +1 -1
  221. package/dist/tools/checkpoint-hook.test.d.ts +2 -0
  222. package/dist/tools/checkpoint-hook.test.d.ts.map +1 -0
  223. package/dist/tools/checkpoint-hook.test.js +65 -0
  224. package/dist/tools/checkpoint-hook.test.js.map +1 -0
  225. package/dist/tools/edit-diff.d.ts +17 -4
  226. package/dist/tools/edit-diff.d.ts.map +1 -1
  227. package/dist/tools/edit-diff.js +78 -16
  228. package/dist/tools/edit-diff.js.map +1 -1
  229. package/dist/tools/edit-diff.test.js +31 -1
  230. package/dist/tools/edit-diff.test.js.map +1 -1
  231. package/dist/tools/edit-robustness.test.d.ts +2 -0
  232. package/dist/tools/edit-robustness.test.d.ts.map +1 -0
  233. package/dist/tools/edit-robustness.test.js +415 -0
  234. package/dist/tools/edit-robustness.test.js.map +1 -0
  235. package/dist/tools/edit.d.ts +8 -2
  236. package/dist/tools/edit.d.ts.map +1 -1
  237. package/dist/tools/edit.js +48 -20
  238. package/dist/tools/edit.js.map +1 -1
  239. package/dist/tools/edit.test.js +23 -22
  240. package/dist/tools/edit.test.js.map +1 -1
  241. package/dist/tools/enter-plan.d.ts +1 -3
  242. package/dist/tools/enter-plan.d.ts.map +1 -1
  243. package/dist/tools/enter-plan.js +9 -21
  244. package/dist/tools/enter-plan.js.map +1 -1
  245. package/dist/tools/exit-plan.d.ts.map +1 -1
  246. package/dist/tools/exit-plan.js +8 -9
  247. package/dist/tools/exit-plan.js.map +1 -1
  248. package/dist/tools/goal-mode.test.d.ts +2 -0
  249. package/dist/tools/goal-mode.test.d.ts.map +1 -0
  250. package/dist/tools/goal-mode.test.js +121 -0
  251. package/dist/tools/goal-mode.test.js.map +1 -0
  252. package/dist/tools/goals.d.ts +14 -2
  253. package/dist/tools/goals.d.ts.map +1 -1
  254. package/dist/tools/goals.js +257 -26
  255. package/dist/tools/goals.js.map +1 -1
  256. package/dist/tools/goals.test.js +365 -1
  257. package/dist/tools/goals.test.js.map +1 -1
  258. package/dist/tools/html-extract.d.ts +58 -0
  259. package/dist/tools/html-extract.d.ts.map +1 -0
  260. package/dist/tools/html-extract.js +130 -0
  261. package/dist/tools/html-extract.js.map +1 -0
  262. package/dist/tools/html-extract.test.d.ts +2 -0
  263. package/dist/tools/html-extract.test.d.ts.map +1 -0
  264. package/dist/tools/html-extract.test.js +60 -0
  265. package/dist/tools/html-extract.test.js.map +1 -0
  266. package/dist/tools/index.d.ts +15 -12
  267. package/dist/tools/index.d.ts.map +1 -1
  268. package/dist/tools/index.js +12 -8
  269. package/dist/tools/index.js.map +1 -1
  270. package/dist/tools/pdf-extract.d.ts +18 -0
  271. package/dist/tools/pdf-extract.d.ts.map +1 -0
  272. package/dist/tools/pdf-extract.js +43 -0
  273. package/dist/tools/pdf-extract.js.map +1 -0
  274. package/dist/tools/pdf-extract.test.d.ts +2 -0
  275. package/dist/tools/pdf-extract.test.d.ts.map +1 -0
  276. package/dist/tools/pdf-extract.test.js +15 -0
  277. package/dist/tools/pdf-extract.test.js.map +1 -0
  278. package/dist/tools/plan-mode.test.js +82 -243
  279. package/dist/tools/plan-mode.test.js.map +1 -1
  280. package/dist/tools/prompt-hints.d.ts.map +1 -1
  281. package/dist/tools/prompt-hints.js +6 -9
  282. package/dist/tools/prompt-hints.js.map +1 -1
  283. package/dist/tools/read-only-bash.d.ts +13 -0
  284. package/dist/tools/read-only-bash.d.ts.map +1 -0
  285. package/dist/tools/read-only-bash.js +155 -0
  286. package/dist/tools/read-only-bash.js.map +1 -0
  287. package/dist/tools/read-only-bash.test.d.ts +2 -0
  288. package/dist/tools/read-only-bash.test.d.ts.map +1 -0
  289. package/dist/tools/read-only-bash.test.js +47 -0
  290. package/dist/tools/read-only-bash.test.js.map +1 -0
  291. package/dist/tools/read.d.ts.map +1 -1
  292. package/dist/tools/read.js +14 -1
  293. package/dist/tools/read.js.map +1 -1
  294. package/dist/tools/read.test.js +4 -0
  295. package/dist/tools/read.test.js.map +1 -1
  296. package/dist/tools/screenshot.d.ts +25 -0
  297. package/dist/tools/screenshot.d.ts.map +1 -0
  298. package/dist/tools/screenshot.js +165 -0
  299. package/dist/tools/screenshot.js.map +1 -0
  300. package/dist/tools/screenshot.test.d.ts +2 -0
  301. package/dist/tools/screenshot.test.d.ts.map +1 -0
  302. package/dist/tools/screenshot.test.js +145 -0
  303. package/dist/tools/screenshot.test.js.map +1 -0
  304. package/dist/tools/subagent.d.ts +7 -2
  305. package/dist/tools/subagent.d.ts.map +1 -1
  306. package/dist/tools/subagent.js +31 -9
  307. package/dist/tools/subagent.js.map +1 -1
  308. package/dist/tools/tasks.d.ts.map +1 -1
  309. package/dist/tools/tasks.js +22 -58
  310. package/dist/tools/tasks.js.map +1 -1
  311. package/dist/tools/web-fetch.d.ts +16 -1
  312. package/dist/tools/web-fetch.d.ts.map +1 -1
  313. package/dist/tools/web-fetch.js +357 -45
  314. package/dist/tools/web-fetch.js.map +1 -1
  315. package/dist/tools/web-fetch.test.js +263 -2
  316. package/dist/tools/web-fetch.test.js.map +1 -1
  317. package/dist/tools/web-search.d.ts +14 -0
  318. package/dist/tools/web-search.d.ts.map +1 -1
  319. package/dist/tools/web-search.js +321 -35
  320. package/dist/tools/web-search.js.map +1 -1
  321. package/dist/tools/web-search.test.js +144 -1
  322. package/dist/tools/web-search.test.js.map +1 -1
  323. package/dist/tools/write.d.ts +8 -2
  324. package/dist/tools/write.d.ts.map +1 -1
  325. package/dist/tools/write.js +36 -9
  326. package/dist/tools/write.js.map +1 -1
  327. package/dist/tools/write.test.js +0 -16
  328. package/dist/tools/write.test.js.map +1 -1
  329. package/dist/ui/App.d.ts +52 -280
  330. package/dist/ui/App.d.ts.map +1 -1
  331. package/dist/ui/App.js +1608 -2235
  332. package/dist/ui/App.js.map +1 -1
  333. package/dist/ui/activity-phrases.d.ts.map +1 -1
  334. package/dist/ui/activity-phrases.js +0 -2
  335. package/dist/ui/activity-phrases.js.map +1 -1
  336. package/dist/ui/app-items.d.ts +276 -0
  337. package/dist/ui/app-items.d.ts.map +1 -0
  338. package/dist/ui/app-items.js +52 -0
  339. package/dist/ui/app-items.js.map +1 -0
  340. package/dist/ui/app-items.test.d.ts +2 -0
  341. package/dist/ui/app-items.test.d.ts.map +1 -0
  342. package/dist/ui/app-items.test.js +38 -0
  343. package/dist/ui/app-items.test.js.map +1 -0
  344. package/dist/ui/app-state-persistence.test.js +45 -160
  345. package/dist/ui/app-state-persistence.test.js.map +1 -1
  346. package/dist/ui/assistant-stream-flush-parity.test.d.ts +2 -0
  347. package/dist/ui/assistant-stream-flush-parity.test.d.ts.map +1 -0
  348. package/dist/ui/assistant-stream-flush-parity.test.js +85 -0
  349. package/dist/ui/assistant-stream-flush-parity.test.js.map +1 -0
  350. package/dist/ui/chat-layout-pinning.test.d.ts +2 -0
  351. package/dist/ui/chat-layout-pinning.test.d.ts.map +1 -0
  352. package/dist/ui/chat-layout-pinning.test.js +525 -0
  353. package/dist/ui/chat-layout-pinning.test.js.map +1 -0
  354. package/dist/ui/components/ActivityIndicator.d.ts +1 -2
  355. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  356. package/dist/ui/components/ActivityIndicator.js +63 -94
  357. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  358. package/dist/ui/components/AnimationContext.d.ts.map +1 -1
  359. package/dist/ui/components/AnimationContext.js +3 -5
  360. package/dist/ui/components/AnimationContext.js.map +1 -1
  361. package/dist/ui/components/AssistantMessage.d.ts +6 -2
  362. package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
  363. package/dist/ui/components/AssistantMessage.js +9 -4
  364. package/dist/ui/components/AssistantMessage.js.map +1 -1
  365. package/dist/ui/components/AssistantMessage.test.d.ts +2 -0
  366. package/dist/ui/components/AssistantMessage.test.d.ts.map +1 -0
  367. package/dist/ui/components/AssistantMessage.test.js +387 -0
  368. package/dist/ui/components/AssistantMessage.test.js.map +1 -0
  369. package/dist/ui/components/BackgroundTasksBar.d.ts +1 -3
  370. package/dist/ui/components/BackgroundTasksBar.d.ts.map +1 -1
  371. package/dist/ui/components/BackgroundTasksBar.js +8 -10
  372. package/dist/ui/components/BackgroundTasksBar.js.map +1 -1
  373. package/dist/ui/components/Banner.d.ts +1 -2
  374. package/dist/ui/components/Banner.d.ts.map +1 -1
  375. package/dist/ui/components/Banner.js +9 -4
  376. package/dist/ui/components/Banner.js.map +1 -1
  377. package/dist/ui/components/ChatFooterPane.d.ts +25 -0
  378. package/dist/ui/components/ChatFooterPane.d.ts.map +1 -0
  379. package/dist/ui/components/ChatFooterPane.js +15 -0
  380. package/dist/ui/components/ChatFooterPane.js.map +1 -0
  381. package/dist/ui/components/ChatInputStack.d.ts +36 -0
  382. package/dist/ui/components/ChatInputStack.d.ts.map +1 -0
  383. package/dist/ui/components/ChatInputStack.js +10 -0
  384. package/dist/ui/components/ChatInputStack.js.map +1 -0
  385. package/dist/ui/components/ChatLayout.d.ts +37 -0
  386. package/dist/ui/components/ChatLayout.d.ts.map +1 -0
  387. package/dist/ui/components/ChatLayout.js +21 -0
  388. package/dist/ui/components/ChatLayout.js.map +1 -0
  389. package/dist/ui/components/ChatLivePane.d.ts +18 -0
  390. package/dist/ui/components/ChatLivePane.d.ts.map +1 -0
  391. package/dist/ui/components/ChatLivePane.js +22 -0
  392. package/dist/ui/components/ChatLivePane.js.map +1 -0
  393. package/dist/ui/components/ChatScreen.d.ts +136 -0
  394. package/dist/ui/components/ChatScreen.d.ts.map +1 -0
  395. package/dist/ui/components/ChatScreen.js +16 -0
  396. package/dist/ui/components/ChatScreen.js.map +1 -0
  397. package/dist/ui/components/ChatStatusRow.d.ts +34 -0
  398. package/dist/ui/components/ChatStatusRow.d.ts.map +1 -0
  399. package/dist/ui/components/ChatStatusRow.js +11 -0
  400. package/dist/ui/components/ChatStatusRow.js.map +1 -0
  401. package/dist/ui/components/CompactionNotice.d.ts +4 -2
  402. package/dist/ui/components/CompactionNotice.d.ts.map +1 -1
  403. package/dist/ui/components/CompactionNotice.js +4 -4
  404. package/dist/ui/components/CompactionNotice.js.map +1 -1
  405. package/dist/ui/components/Footer.d.ts +27 -6
  406. package/dist/ui/components/Footer.d.ts.map +1 -1
  407. package/dist/ui/components/Footer.js +75 -30
  408. package/dist/ui/components/Footer.js.map +1 -1
  409. package/dist/ui/components/FooterStatusRow.d.ts +20 -0
  410. package/dist/ui/components/FooterStatusRow.d.ts.map +1 -0
  411. package/dist/ui/components/FooterStatusRow.js +10 -0
  412. package/dist/ui/components/FooterStatusRow.js.map +1 -0
  413. package/dist/ui/components/FullScreenOverlayRouter.d.ts +19 -0
  414. package/dist/ui/components/FullScreenOverlayRouter.d.ts.map +1 -0
  415. package/dist/ui/components/FullScreenOverlayRouter.js +18 -0
  416. package/dist/ui/components/FullScreenOverlayRouter.js.map +1 -0
  417. package/dist/ui/components/GoalOverlay.d.ts +28 -20
  418. package/dist/ui/components/GoalOverlay.d.ts.map +1 -1
  419. package/dist/ui/components/GoalOverlay.js +283 -253
  420. package/dist/ui/components/GoalOverlay.js.map +1 -1
  421. package/dist/ui/components/GoalStatusBar.d.ts.map +1 -1
  422. package/dist/ui/components/GoalStatusBar.js +2 -1
  423. package/dist/ui/components/GoalStatusBar.js.map +1 -1
  424. package/dist/ui/components/IdealHookMessage.d.ts +14 -0
  425. package/dist/ui/components/IdealHookMessage.d.ts.map +1 -0
  426. package/dist/ui/components/IdealHookMessage.js +26 -0
  427. package/dist/ui/components/IdealHookMessage.js.map +1 -0
  428. package/dist/ui/components/InputArea.d.ts +31 -6
  429. package/dist/ui/components/InputArea.d.ts.map +1 -1
  430. package/dist/ui/components/InputArea.js +208 -50
  431. package/dist/ui/components/InputArea.js.map +1 -1
  432. package/dist/ui/components/InputArea.test.js +11 -1
  433. package/dist/ui/components/InputArea.test.js.map +1 -1
  434. package/dist/ui/components/LiveToolPanel.d.ts +30 -0
  435. package/dist/ui/components/LiveToolPanel.d.ts.map +1 -0
  436. package/dist/ui/components/LiveToolPanel.js +66 -0
  437. package/dist/ui/components/LiveToolPanel.js.map +1 -0
  438. package/dist/ui/components/Markdown.d.ts +11 -11
  439. package/dist/ui/components/Markdown.d.ts.map +1 -1
  440. package/dist/ui/components/Markdown.js +28 -198
  441. package/dist/ui/components/Markdown.js.map +1 -1
  442. package/dist/ui/components/ModelSelector.d.ts.map +1 -1
  443. package/dist/ui/components/ModelSelector.js +18 -18
  444. package/dist/ui/components/ModelSelector.js.map +1 -1
  445. package/dist/ui/components/PlanModeLogo.d.ts +3 -0
  446. package/dist/ui/components/PlanModeLogo.d.ts.map +1 -0
  447. package/dist/ui/components/PlanModeLogo.js +49 -0
  448. package/dist/ui/components/PlanModeLogo.js.map +1 -0
  449. package/dist/ui/components/PlanOverlay.d.ts +7 -0
  450. package/dist/ui/components/PlanOverlay.d.ts.map +1 -1
  451. package/dist/ui/components/PlanOverlay.js +17 -3
  452. package/dist/ui/components/PlanOverlay.js.map +1 -1
  453. package/dist/ui/components/PlanOverlay.test.d.ts +2 -0
  454. package/dist/ui/components/PlanOverlay.test.d.ts.map +1 -0
  455. package/dist/ui/components/PlanOverlay.test.js +24 -0
  456. package/dist/ui/components/PlanOverlay.test.js.map +1 -0
  457. package/dist/ui/components/QueueIndicator.d.ts +9 -0
  458. package/dist/ui/components/QueueIndicator.d.ts.map +1 -0
  459. package/dist/ui/components/QueueIndicator.js +9 -0
  460. package/dist/ui/components/QueueIndicator.js.map +1 -0
  461. package/dist/ui/components/RewindOverlay.d.ts +18 -0
  462. package/dist/ui/components/RewindOverlay.d.ts.map +1 -0
  463. package/dist/ui/components/RewindOverlay.js +52 -0
  464. package/dist/ui/components/RewindOverlay.js.map +1 -0
  465. package/dist/ui/components/SelectList.d.ts.map +1 -1
  466. package/dist/ui/components/SelectList.js +5 -0
  467. package/dist/ui/components/SelectList.js.map +1 -1
  468. package/dist/ui/components/ServerToolExecution.d.ts +2 -0
  469. package/dist/ui/components/ServerToolExecution.d.ts.map +1 -1
  470. package/dist/ui/components/ServerToolExecution.js +7 -23
  471. package/dist/ui/components/ServerToolExecution.js.map +1 -1
  472. package/dist/ui/components/SessionSummary.d.ts +5 -0
  473. package/dist/ui/components/SessionSummary.d.ts.map +1 -0
  474. package/dist/ui/components/SessionSummary.js +32 -0
  475. package/dist/ui/components/SessionSummary.js.map +1 -0
  476. package/dist/ui/components/SlashCommandMenu.d.ts +4 -3
  477. package/dist/ui/components/SlashCommandMenu.d.ts.map +1 -1
  478. package/dist/ui/components/SlashCommandMenu.js +38 -26
  479. package/dist/ui/components/SlashCommandMenu.js.map +1 -1
  480. package/dist/ui/components/SlashStyledSelectList.d.ts +21 -0
  481. package/dist/ui/components/SlashStyledSelectList.d.ts.map +1 -0
  482. package/dist/ui/components/SlashStyledSelectList.js +71 -0
  483. package/dist/ui/components/SlashStyledSelectList.js.map +1 -0
  484. package/dist/ui/components/StreamingArea.d.ts +11 -2
  485. package/dist/ui/components/StreamingArea.d.ts.map +1 -1
  486. package/dist/ui/components/StreamingArea.js +20 -23
  487. package/dist/ui/components/StreamingArea.js.map +1 -1
  488. package/dist/ui/components/StreamingArea.test.d.ts +2 -0
  489. package/dist/ui/components/StreamingArea.test.d.ts.map +1 -0
  490. package/dist/ui/components/StreamingArea.test.js +18 -0
  491. package/dist/ui/components/StreamingArea.test.js.map +1 -0
  492. package/dist/ui/components/SubAgentPanel.d.ts +2 -1
  493. package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
  494. package/dist/ui/components/SubAgentPanel.js +4 -3
  495. package/dist/ui/components/SubAgentPanel.js.map +1 -1
  496. package/dist/ui/components/TaskOverlay.d.ts.map +1 -1
  497. package/dist/ui/components/TaskOverlay.js +53 -82
  498. package/dist/ui/components/TaskOverlay.js.map +1 -1
  499. package/dist/ui/components/TaskPickerMenu.d.ts +9 -0
  500. package/dist/ui/components/TaskPickerMenu.d.ts.map +1 -0
  501. package/dist/ui/components/TaskPickerMenu.js +33 -0
  502. package/dist/ui/components/TaskPickerMenu.js.map +1 -0
  503. package/dist/ui/components/ThemeSelector.js +2 -2
  504. package/dist/ui/components/ThemeSelector.js.map +1 -1
  505. package/dist/ui/components/ToolExecution.d.ts +2 -0
  506. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  507. package/dist/ui/components/ToolExecution.js +18 -52
  508. package/dist/ui/components/ToolExecution.js.map +1 -1
  509. package/dist/ui/components/ToolGroupExecution.d.ts +6 -2
  510. package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -1
  511. package/dist/ui/components/ToolGroupExecution.js +15 -147
  512. package/dist/ui/components/ToolGroupExecution.js.map +1 -1
  513. package/dist/ui/components/ToolUseLoader.d.ts +3 -1
  514. package/dist/ui/components/ToolUseLoader.d.ts.map +1 -1
  515. package/dist/ui/components/ToolUseLoader.js +2 -2
  516. package/dist/ui/components/ToolUseLoader.js.map +1 -1
  517. package/dist/ui/components/TranscriptViewport.d.ts +34 -0
  518. package/dist/ui/components/TranscriptViewport.d.ts.map +1 -0
  519. package/dist/ui/components/TranscriptViewport.js +45 -0
  520. package/dist/ui/components/TranscriptViewport.js.map +1 -0
  521. package/dist/ui/components/UserMessage.d.ts.map +1 -1
  522. package/dist/ui/components/UserMessage.js +15 -10
  523. package/dist/ui/components/UserMessage.js.map +1 -1
  524. package/dist/ui/components/UserMessage.test.d.ts +2 -0
  525. package/dist/ui/components/UserMessage.test.d.ts.map +1 -0
  526. package/dist/ui/components/UserMessage.test.js +39 -0
  527. package/dist/ui/components/UserMessage.test.js.map +1 -0
  528. package/dist/ui/components/index.d.ts +5 -0
  529. package/dist/ui/components/index.d.ts.map +1 -1
  530. package/dist/ui/components/index.js +5 -0
  531. package/dist/ui/components/index.js.map +1 -1
  532. package/dist/ui/duration-format.d.ts +2 -0
  533. package/dist/ui/duration-format.d.ts.map +1 -0
  534. package/dist/ui/duration-format.js +9 -0
  535. package/dist/ui/duration-format.js.map +1 -0
  536. package/dist/ui/duration-summary.d.ts +2 -0
  537. package/dist/ui/duration-summary.d.ts.map +1 -0
  538. package/dist/ui/duration-summary.js +66 -0
  539. package/dist/ui/duration-summary.js.map +1 -0
  540. package/dist/ui/error-item.d.ts +8 -0
  541. package/dist/ui/error-item.d.ts.map +1 -0
  542. package/dist/ui/error-item.js +32 -0
  543. package/dist/ui/error-item.js.map +1 -0
  544. package/dist/ui/footer-jump-regression.test.d.ts +2 -0
  545. package/dist/ui/footer-jump-regression.test.d.ts.map +1 -0
  546. package/dist/ui/footer-jump-regression.test.js +177 -0
  547. package/dist/ui/footer-jump-regression.test.js.map +1 -0
  548. package/dist/ui/footer-status-layout.test.js +22 -7
  549. package/dist/ui/footer-status-layout.test.js.map +1 -1
  550. package/dist/ui/goal-events.d.ts.map +1 -1
  551. package/dist/ui/goal-events.js +14 -6
  552. package/dist/ui/goal-events.js.map +1 -1
  553. package/dist/ui/goal-events.test.js +40 -2
  554. package/dist/ui/goal-events.test.js.map +1 -1
  555. package/dist/ui/goal-lifecycle-orchestration.test.d.ts +2 -0
  556. package/dist/ui/goal-lifecycle-orchestration.test.d.ts.map +1 -0
  557. package/dist/ui/goal-lifecycle-orchestration.test.js +512 -0
  558. package/dist/ui/goal-lifecycle-orchestration.test.js.map +1 -0
  559. package/dist/ui/goal-overlay.test.js +122 -44
  560. package/dist/ui/goal-overlay.test.js.map +1 -1
  561. package/dist/ui/goal-status-bar.test.js +29 -0
  562. package/dist/ui/goal-status-bar.test.js.map +1 -1
  563. package/dist/ui/goal-summary.d.ts +14 -0
  564. package/dist/ui/goal-summary.d.ts.map +1 -0
  565. package/dist/ui/goal-summary.js +194 -0
  566. package/dist/ui/goal-summary.js.map +1 -0
  567. package/dist/ui/hooks/useAgentLoop.d.ts +25 -4
  568. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  569. package/dist/ui/hooks/useAgentLoop.js +197 -22
  570. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  571. package/dist/ui/hooks/useAgentLoop.test.d.ts +2 -0
  572. package/dist/ui/hooks/useAgentLoop.test.d.ts.map +1 -0
  573. package/dist/ui/hooks/useAgentLoop.test.js +8 -0
  574. package/dist/ui/hooks/useAgentLoop.test.js.map +1 -0
  575. package/dist/ui/hooks/useChatLayoutMeasurements.d.ts +45 -0
  576. package/dist/ui/hooks/useChatLayoutMeasurements.d.ts.map +1 -0
  577. package/dist/ui/hooks/useChatLayoutMeasurements.js +97 -0
  578. package/dist/ui/hooks/useChatLayoutMeasurements.js.map +1 -0
  579. package/dist/ui/hooks/useContextCompaction.d.ts +41 -0
  580. package/dist/ui/hooks/useContextCompaction.d.ts.map +1 -0
  581. package/dist/ui/hooks/useContextCompaction.js +149 -0
  582. package/dist/ui/hooks/useContextCompaction.js.map +1 -0
  583. package/dist/ui/hooks/useModeState.d.ts +56 -0
  584. package/dist/ui/hooks/useModeState.d.ts.map +1 -0
  585. package/dist/ui/hooks/useModeState.js +65 -0
  586. package/dist/ui/hooks/useModeState.js.map +1 -0
  587. package/dist/ui/hooks/usePixelFixFlow.d.ts +57 -0
  588. package/dist/ui/hooks/usePixelFixFlow.d.ts.map +1 -0
  589. package/dist/ui/hooks/usePixelFixFlow.js +102 -0
  590. package/dist/ui/hooks/usePixelFixFlow.js.map +1 -0
  591. package/dist/ui/hooks/useSessionPersistence.d.ts +34 -0
  592. package/dist/ui/hooks/useSessionPersistence.d.ts.map +1 -0
  593. package/dist/ui/hooks/useSessionPersistence.js +67 -0
  594. package/dist/ui/hooks/useSessionPersistence.js.map +1 -0
  595. package/dist/ui/hooks/useTaskPickerController.d.ts +19 -0
  596. package/dist/ui/hooks/useTaskPickerController.d.ts.map +1 -0
  597. package/dist/ui/hooks/useTaskPickerController.js +41 -0
  598. package/dist/ui/hooks/useTaskPickerController.js.map +1 -0
  599. package/dist/ui/hooks/useTerminalSize.d.ts +13 -10
  600. package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
  601. package/dist/ui/hooks/useTerminalSize.js +21 -16
  602. package/dist/ui/hooks/useTerminalSize.js.map +1 -1
  603. package/dist/ui/hooks/useTranscriptHistory.d.ts +48 -0
  604. package/dist/ui/hooks/useTranscriptHistory.d.ts.map +1 -0
  605. package/dist/ui/hooks/useTranscriptHistory.js +113 -0
  606. package/dist/ui/hooks/useTranscriptHistory.js.map +1 -0
  607. package/dist/ui/hooks/useTranscriptHistory.test.d.ts +2 -0
  608. package/dist/ui/hooks/useTranscriptHistory.test.d.ts.map +1 -0
  609. package/dist/ui/hooks/useTranscriptHistory.test.js +61 -0
  610. package/dist/ui/hooks/useTranscriptHistory.test.js.map +1 -0
  611. package/dist/ui/hooks/useTranscriptScroll.d.ts +30 -0
  612. package/dist/ui/hooks/useTranscriptScroll.d.ts.map +1 -0
  613. package/dist/ui/hooks/useTranscriptScroll.js +44 -0
  614. package/dist/ui/hooks/useTranscriptScroll.js.map +1 -0
  615. package/dist/ui/hooks/useTranscriptScroll.test.d.ts +2 -0
  616. package/dist/ui/hooks/useTranscriptScroll.test.d.ts.map +1 -0
  617. package/dist/ui/hooks/useTranscriptScroll.test.js +119 -0
  618. package/dist/ui/hooks/useTranscriptScroll.test.js.map +1 -0
  619. package/dist/ui/item-helpers.d.ts +26 -0
  620. package/dist/ui/item-helpers.d.ts.map +1 -0
  621. package/dist/ui/item-helpers.js +114 -0
  622. package/dist/ui/item-helpers.js.map +1 -0
  623. package/dist/ui/layout-decisions.d.ts +81 -0
  624. package/dist/ui/layout-decisions.d.ts.map +1 -0
  625. package/dist/ui/layout-decisions.js +89 -0
  626. package/dist/ui/layout-decisions.js.map +1 -0
  627. package/dist/ui/live-area-clamp.test.d.ts +2 -0
  628. package/dist/ui/live-area-clamp.test.d.ts.map +1 -0
  629. package/dist/ui/live-area-clamp.test.js +88 -0
  630. package/dist/ui/live-area-clamp.test.js.map +1 -0
  631. package/dist/ui/live-area-height.d.ts +35 -0
  632. package/dist/ui/live-area-height.d.ts.map +1 -0
  633. package/dist/ui/live-area-height.js +80 -0
  634. package/dist/ui/live-area-height.js.map +1 -0
  635. package/dist/ui/live-area-height.test.d.ts +2 -0
  636. package/dist/ui/live-area-height.test.d.ts.map +1 -0
  637. package/dist/ui/live-area-height.test.js +67 -0
  638. package/dist/ui/live-area-height.test.js.map +1 -0
  639. package/dist/ui/live-frame-height.test.d.ts +2 -0
  640. package/dist/ui/live-frame-height.test.d.ts.map +1 -0
  641. package/dist/ui/live-frame-height.test.js +116 -0
  642. package/dist/ui/live-frame-height.test.js.map +1 -0
  643. package/dist/ui/live-item-flush.d.ts +2 -2
  644. package/dist/ui/live-item-flush.d.ts.map +1 -1
  645. package/dist/ui/live-item-flush.js +8 -4
  646. package/dist/ui/live-item-flush.js.map +1 -1
  647. package/dist/ui/login.js +1 -1
  648. package/dist/ui/long-prompt-regression-harness.test.d.ts +2 -0
  649. package/dist/ui/long-prompt-regression-harness.test.d.ts.map +1 -0
  650. package/dist/ui/long-prompt-regression-harness.test.js +195 -0
  651. package/dist/ui/long-prompt-regression-harness.test.js.map +1 -0
  652. package/dist/ui/mcp.d.ts +62 -0
  653. package/dist/ui/mcp.d.ts.map +1 -0
  654. package/dist/ui/mcp.js +306 -0
  655. package/dist/ui/mcp.js.map +1 -0
  656. package/dist/ui/plan-overlay.test.js +7 -29
  657. package/dist/ui/plan-overlay.test.js.map +1 -1
  658. package/dist/ui/prompt-routing.d.ts +11 -0
  659. package/dist/ui/prompt-routing.d.ts.map +1 -0
  660. package/dist/ui/prompt-routing.js +61 -0
  661. package/dist/ui/prompt-routing.js.map +1 -0
  662. package/dist/ui/queued-message.test.d.ts +2 -0
  663. package/dist/ui/queued-message.test.d.ts.map +1 -0
  664. package/dist/ui/queued-message.test.js +162 -0
  665. package/dist/ui/queued-message.test.js.map +1 -0
  666. package/dist/ui/render.d.ts +38 -42
  667. package/dist/ui/render.d.ts.map +1 -1
  668. package/dist/ui/render.js +128 -42
  669. package/dist/ui/render.js.map +1 -1
  670. package/dist/ui/render.test.d.ts +2 -0
  671. package/dist/ui/render.test.d.ts.map +1 -0
  672. package/dist/ui/render.test.js +16 -0
  673. package/dist/ui/render.test.js.map +1 -0
  674. package/dist/ui/scroll-stabilization.test.js +1 -7
  675. package/dist/ui/scroll-stabilization.test.js.map +1 -1
  676. package/dist/ui/session-summary.d.ts +63 -0
  677. package/dist/ui/session-summary.d.ts.map +1 -0
  678. package/dist/ui/session-summary.js +81 -0
  679. package/dist/ui/session-summary.js.map +1 -0
  680. package/dist/ui/slash-command-images.test.js +12 -11
  681. package/dist/ui/slash-command-images.test.js.map +1 -1
  682. package/dist/ui/stores/transcript-scroll-store.d.ts +27 -0
  683. package/dist/ui/stores/transcript-scroll-store.d.ts.map +1 -0
  684. package/dist/ui/stores/transcript-scroll-store.js +73 -0
  685. package/dist/ui/stores/transcript-scroll-store.js.map +1 -0
  686. package/dist/ui/stores/transcript-scroll-store.test.d.ts +2 -0
  687. package/dist/ui/stores/transcript-scroll-store.test.d.ts.map +1 -0
  688. package/dist/ui/stores/transcript-scroll-store.test.js +73 -0
  689. package/dist/ui/stores/transcript-scroll-store.test.js.map +1 -0
  690. package/dist/ui/streaming-flush-bounce.test.d.ts +2 -0
  691. package/dist/ui/streaming-flush-bounce.test.d.ts.map +1 -0
  692. package/dist/ui/streaming-flush-bounce.test.js +156 -0
  693. package/dist/ui/streaming-flush-bounce.test.js.map +1 -0
  694. package/dist/ui/submit-prompt-command.d.ts +49 -0
  695. package/dist/ui/submit-prompt-command.d.ts.map +1 -0
  696. package/dist/ui/submit-prompt-command.js +107 -0
  697. package/dist/ui/submit-prompt-command.js.map +1 -0
  698. package/dist/ui/submit-slash-commands.d.ts +12 -0
  699. package/dist/ui/submit-slash-commands.d.ts.map +1 -0
  700. package/dist/ui/submit-slash-commands.js +32 -0
  701. package/dist/ui/submit-slash-commands.js.map +1 -0
  702. package/dist/ui/terminal-history-format.d.ts +43 -0
  703. package/dist/ui/terminal-history-format.d.ts.map +1 -0
  704. package/dist/ui/terminal-history-format.js +133 -0
  705. package/dist/ui/terminal-history-format.js.map +1 -0
  706. package/dist/ui/terminal-history-retry-dedup.test.d.ts +2 -0
  707. package/dist/ui/terminal-history-retry-dedup.test.d.ts.map +1 -0
  708. package/dist/ui/terminal-history-retry-dedup.test.js +61 -0
  709. package/dist/ui/terminal-history-retry-dedup.test.js.map +1 -0
  710. package/dist/ui/terminal-history-spacing.d.ts +2 -0
  711. package/dist/ui/terminal-history-spacing.d.ts.map +1 -0
  712. package/dist/ui/terminal-history-spacing.js +2 -0
  713. package/dist/ui/terminal-history-spacing.js.map +1 -0
  714. package/dist/ui/terminal-history-status-renderers.d.ts +15 -0
  715. package/dist/ui/terminal-history-status-renderers.d.ts.map +1 -0
  716. package/dist/ui/terminal-history-status-renderers.js +99 -0
  717. package/dist/ui/terminal-history-status-renderers.js.map +1 -0
  718. package/dist/ui/terminal-history.d.ts +26 -0
  719. package/dist/ui/terminal-history.d.ts.map +1 -0
  720. package/dist/ui/terminal-history.js +916 -0
  721. package/dist/ui/terminal-history.js.map +1 -0
  722. package/dist/ui/terminal-history.test.d.ts +2 -0
  723. package/dist/ui/terminal-history.test.d.ts.map +1 -0
  724. package/dist/ui/terminal-history.test.js +550 -0
  725. package/dist/ui/terminal-history.test.js.map +1 -0
  726. package/dist/ui/testing/screen-recorder.d.ts +29 -0
  727. package/dist/ui/testing/screen-recorder.d.ts.map +1 -0
  728. package/dist/ui/testing/screen-recorder.js +179 -0
  729. package/dist/ui/testing/screen-recorder.js.map +1 -0
  730. package/dist/ui/thinking-level-cycle.test.d.ts +2 -0
  731. package/dist/ui/thinking-level-cycle.test.d.ts.map +1 -0
  732. package/dist/ui/thinking-level-cycle.test.js +45 -0
  733. package/dist/ui/thinking-level-cycle.test.js.map +1 -0
  734. package/dist/ui/thinking-level.d.ts +2 -0
  735. package/dist/ui/thinking-level.d.ts.map +1 -0
  736. package/dist/ui/thinking-level.js +2 -0
  737. package/dist/ui/thinking-level.js.map +1 -0
  738. package/dist/ui/tool-group-summary.d.ts +24 -0
  739. package/dist/ui/tool-group-summary.d.ts.map +1 -0
  740. package/dist/ui/tool-group-summary.js +233 -0
  741. package/dist/ui/tool-group-summary.js.map +1 -0
  742. package/dist/ui/tool-group-summary.test.d.ts +2 -0
  743. package/dist/ui/tool-group-summary.test.d.ts.map +1 -0
  744. package/dist/ui/tool-group-summary.test.js +79 -0
  745. package/dist/ui/tool-group-summary.test.js.map +1 -0
  746. package/dist/ui/tool-line-summary.d.ts +29 -0
  747. package/dist/ui/tool-line-summary.d.ts.map +1 -0
  748. package/dist/ui/tool-line-summary.js +153 -0
  749. package/dist/ui/tool-line-summary.js.map +1 -0
  750. package/dist/ui/transcript/MiscRows.d.ts +26 -0
  751. package/dist/ui/transcript/MiscRows.d.ts.map +1 -0
  752. package/dist/ui/transcript/MiscRows.js +68 -0
  753. package/dist/ui/transcript/MiscRows.js.map +1 -0
  754. package/dist/ui/transcript/StatusRow.d.ts +14 -0
  755. package/dist/ui/transcript/StatusRow.d.ts.map +1 -0
  756. package/dist/ui/transcript/StatusRow.js +14 -0
  757. package/dist/ui/transcript/StatusRow.js.map +1 -0
  758. package/dist/ui/transcript/ToolRows.d.ts +20 -0
  759. package/dist/ui/transcript/ToolRows.d.ts.map +1 -0
  760. package/dist/ui/transcript/ToolRows.js +25 -0
  761. package/dist/ui/transcript/ToolRows.js.map +1 -0
  762. package/dist/ui/transcript/TranscriptItemFrame.d.ts +8 -0
  763. package/dist/ui/transcript/TranscriptItemFrame.d.ts.map +1 -0
  764. package/dist/ui/transcript/TranscriptItemFrame.js +9 -0
  765. package/dist/ui/transcript/TranscriptItemFrame.js.map +1 -0
  766. package/dist/ui/transcript/TranscriptRenderer.d.ts +22 -0
  767. package/dist/ui/transcript/TranscriptRenderer.d.ts.map +1 -0
  768. package/dist/ui/transcript/TranscriptRenderer.js +111 -0
  769. package/dist/ui/transcript/TranscriptRenderer.js.map +1 -0
  770. package/dist/ui/transcript/presentation.d.ts +61 -0
  771. package/dist/ui/transcript/presentation.d.ts.map +1 -0
  772. package/dist/ui/transcript/presentation.js +86 -0
  773. package/dist/ui/transcript/presentation.js.map +1 -0
  774. package/dist/ui/transcript/spacing.d.ts +56 -0
  775. package/dist/ui/transcript/spacing.d.ts.map +1 -0
  776. package/dist/ui/transcript/spacing.js +129 -0
  777. package/dist/ui/transcript/spacing.js.map +1 -0
  778. package/dist/ui/transcript/spacing.test.d.ts +2 -0
  779. package/dist/ui/transcript/spacing.test.d.ts.map +1 -0
  780. package/dist/ui/transcript/spacing.test.js +193 -0
  781. package/dist/ui/transcript/spacing.test.js.map +1 -0
  782. package/dist/ui/transcript/tool-presentation.d.ts +12 -0
  783. package/dist/ui/transcript/tool-presentation.d.ts.map +1 -0
  784. package/dist/ui/transcript/tool-presentation.js +55 -0
  785. package/dist/ui/transcript/tool-presentation.js.map +1 -0
  786. package/dist/ui/transcript/transcript-lines.d.ts +32 -0
  787. package/dist/ui/transcript/transcript-lines.d.ts.map +1 -0
  788. package/dist/ui/transcript/transcript-lines.js +86 -0
  789. package/dist/ui/transcript/transcript-lines.js.map +1 -0
  790. package/dist/ui/transcript/transcript-lines.test.d.ts +2 -0
  791. package/dist/ui/transcript/transcript-lines.test.d.ts.map +1 -0
  792. package/dist/ui/transcript/transcript-lines.test.js +76 -0
  793. package/dist/ui/transcript/transcript-lines.test.js.map +1 -0
  794. package/dist/ui/transcript-viewport-pinning.test.d.ts +2 -0
  795. package/dist/ui/transcript-viewport-pinning.test.d.ts.map +1 -0
  796. package/dist/ui/transcript-viewport-pinning.test.js +148 -0
  797. package/dist/ui/transcript-viewport-pinning.test.js.map +1 -0
  798. package/dist/ui/tui-history-parity.test.d.ts +2 -0
  799. package/dist/ui/tui-history-parity.test.d.ts.map +1 -0
  800. package/dist/ui/tui-history-parity.test.js +381 -0
  801. package/dist/ui/tui-history-parity.test.js.map +1 -0
  802. package/dist/ui/tui-simulation.test.d.ts +2 -0
  803. package/dist/ui/tui-simulation.test.d.ts.map +1 -0
  804. package/dist/ui/tui-simulation.test.js +139 -0
  805. package/dist/ui/tui-simulation.test.js.map +1 -0
  806. package/dist/ui/tui-terminal-recorder.test.d.ts +2 -0
  807. package/dist/ui/tui-terminal-recorder.test.d.ts.map +1 -0
  808. package/dist/ui/tui-terminal-recorder.test.js +134 -0
  809. package/dist/ui/tui-terminal-recorder.test.js.map +1 -0
  810. package/dist/ui/utils/assistant-stream-split.d.ts +23 -0
  811. package/dist/ui/utils/assistant-stream-split.d.ts.map +1 -0
  812. package/dist/ui/utils/assistant-stream-split.js +41 -0
  813. package/dist/ui/utils/assistant-stream-split.js.map +1 -0
  814. package/dist/ui/utils/assistant-stream-split.test.d.ts +2 -0
  815. package/dist/ui/utils/assistant-stream-split.test.d.ts.map +1 -0
  816. package/dist/ui/utils/assistant-stream-split.test.js +40 -0
  817. package/dist/ui/utils/assistant-stream-split.test.js.map +1 -0
  818. package/dist/ui/utils/latex-to-unicode.d.ts +22 -0
  819. package/dist/ui/utils/latex-to-unicode.d.ts.map +1 -0
  820. package/dist/ui/utils/latex-to-unicode.js +538 -0
  821. package/dist/ui/utils/latex-to-unicode.js.map +1 -0
  822. package/dist/ui/utils/markdown-renderer.d.ts +20 -0
  823. package/dist/ui/utils/markdown-renderer.d.ts.map +1 -0
  824. package/dist/ui/utils/markdown-renderer.js +327 -0
  825. package/dist/ui/utils/markdown-renderer.js.map +1 -0
  826. package/dist/ui/utils/markdown-table.d.ts +9 -0
  827. package/dist/ui/utils/markdown-table.d.ts.map +1 -0
  828. package/dist/ui/utils/markdown-table.js +95 -0
  829. package/dist/ui/utils/markdown-table.js.map +1 -0
  830. package/dist/ui/utils/terminal-graphics.d.ts +16 -0
  831. package/dist/ui/utils/terminal-graphics.d.ts.map +1 -0
  832. package/dist/ui/utils/terminal-graphics.js +68 -0
  833. package/dist/ui/utils/terminal-graphics.js.map +1 -0
  834. package/dist/ui/utils/terminal-graphics.test.d.ts +2 -0
  835. package/dist/ui/utils/terminal-graphics.test.d.ts.map +1 -0
  836. package/dist/ui/utils/terminal-graphics.test.js +61 -0
  837. package/dist/ui/utils/terminal-graphics.test.js.map +1 -0
  838. package/dist/ui/utils/terminal-input.d.ts +3 -0
  839. package/dist/ui/utils/terminal-input.d.ts.map +1 -0
  840. package/dist/ui/utils/terminal-input.js +28 -0
  841. package/dist/ui/utils/terminal-input.js.map +1 -0
  842. package/dist/ui/utils/terminal-input.test.d.ts +2 -0
  843. package/dist/ui/utils/terminal-input.test.d.ts.map +1 -0
  844. package/dist/ui/utils/terminal-input.test.js +15 -0
  845. package/dist/ui/utils/terminal-input.test.js.map +1 -0
  846. package/dist/ui/utils/text-utils.d.ts +8 -0
  847. package/dist/ui/utils/text-utils.d.ts.map +1 -0
  848. package/dist/ui/utils/text-utils.js +16 -0
  849. package/dist/ui/utils/text-utils.js.map +1 -0
  850. package/dist/ui/utils/token-to-ansi.js +19 -9
  851. package/dist/ui/utils/token-to-ansi.js.map +1 -1
  852. package/dist/ui/utils/user-message-display.d.ts +7 -0
  853. package/dist/ui/utils/user-message-display.d.ts.map +1 -0
  854. package/dist/ui/utils/user-message-display.js +26 -0
  855. package/dist/ui/utils/user-message-display.js.map +1 -0
  856. package/dist/utils/format.js +7 -9
  857. package/dist/utils/format.js.map +1 -1
  858. package/dist/utils/image.d.ts +9 -0
  859. package/dist/utils/image.d.ts.map +1 -1
  860. package/dist/utils/image.js +25 -0
  861. package/dist/utils/image.js.map +1 -1
  862. package/dist/utils/plan-steps.d.ts.map +1 -1
  863. package/dist/utils/plan-steps.js +5 -1
  864. package/dist/utils/plan-steps.js.map +1 -1
  865. package/dist/utils/plan-steps.test.d.ts +2 -0
  866. package/dist/utils/plan-steps.test.d.ts.map +1 -0
  867. package/dist/utils/plan-steps.test.js +16 -0
  868. package/dist/utils/plan-steps.test.js.map +1 -0
  869. package/package.json +50 -10
  870. package/dist/core/repomap-context.d.ts +0 -11
  871. package/dist/core/repomap-context.d.ts.map +0 -1
  872. package/dist/core/repomap-context.js +0 -68
  873. package/dist/core/repomap-context.js.map +0 -1
  874. package/dist/core/repomap-context.test.d.ts +0 -2
  875. package/dist/core/repomap-context.test.d.ts.map +0 -1
  876. package/dist/core/repomap-context.test.js +0 -47
  877. package/dist/core/repomap-context.test.js.map +0 -1
  878. package/dist/core/repomap.d.ts +0 -74
  879. package/dist/core/repomap.d.ts.map +0 -1
  880. package/dist/core/repomap.js +0 -906
  881. package/dist/core/repomap.js.map +0 -1
  882. package/dist/core/repomap.test.d.ts +0 -2
  883. package/dist/core/repomap.test.d.ts.map +0 -1
  884. package/dist/core/repomap.test.js +0 -494
  885. package/dist/core/repomap.test.js.map +0 -1
  886. package/dist/ui/components/EyesOverlay.d.ts +0 -10
  887. package/dist/ui/components/EyesOverlay.d.ts.map +0 -1
  888. package/dist/ui/components/EyesOverlay.js +0 -220
  889. package/dist/ui/components/EyesOverlay.js.map +0 -1
package/dist/ui/App.js CHANGED
@@ -1,254 +1,149 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { useState, useRef, useCallback, useEffect, useMemo } from "react";
3
- import { Box, Text, Static } from "ink";
3
+ import { Box, useStdout } from "ink";
4
4
  import { useTerminalSize } from "./hooks/useTerminalSize.js";
5
+ import { useChatLayoutMeasurements } from "./hooks/useChatLayoutMeasurements.js";
6
+ import { useTaskPickerController } from "./hooks/useTaskPickerController.js";
7
+ import { useModeState } from "./hooks/useModeState.js";
8
+ import { useSessionPersistence } from "./hooks/useSessionPersistence.js";
9
+ import { useContextCompaction } from "./hooks/useContextCompaction.js";
10
+ import { usePixelFixFlow } from "./hooks/usePixelFixFlow.js";
5
11
  import { useDoublePress } from "./hooks/useDoublePress.js";
6
12
  import { useTaskBarStore, useTaskBarPolling, focusTaskBar, exitTaskBar, expandTaskBar, collapseTaskBar, navigateTaskBar, killTask, } from "./stores/taskbar-store.js";
7
- import { createHash } from "node:crypto";
8
- import { readFileSync, writeFileSync } from "node:fs";
9
- import { homedir } from "node:os";
10
- import { join } from "node:path";
11
13
  import { playNotificationSound } from "../utils/sound.js";
12
- import { formatError, } from "@prestyj/ai";
13
- import { extractImagePaths } from "../utils/image.js";
14
+ import {} from "@prestyj/ai";
15
+ import { downscaleForPreview, extractImagePaths } from "../utils/image.js";
14
16
  import { useAgentLoop } from "./hooks/useAgentLoop.js";
15
- import { isEyesActive, journalCount } from "@prestyj/eyes";
16
- import { UserMessage } from "./components/UserMessage.js";
17
- import { AssistantMessage } from "./components/AssistantMessage.js";
18
- import { ToolExecution } from "./components/ToolExecution.js";
19
- import { ToolGroupExecution } from "./components/ToolGroupExecution.js";
20
- import { ServerToolExecution } from "./components/ServerToolExecution.js";
21
- import { SubAgentPanel } from "./components/SubAgentPanel.js";
22
- import { CompactionSpinner, CompactionDone } from "./components/CompactionNotice.js";
17
+ import { useTranscriptHistory } from "./hooks/useTranscriptHistory.js";
23
18
  import { createWebSearchTool } from "../tools/web-search.js";
24
- import { StreamingArea } from "./components/StreamingArea.js";
25
- import { ActivityIndicator } from "./components/ActivityIndicator.js";
26
- import { InputArea } from "./components/InputArea.js";
27
- import { Footer } from "./components/Footer.js";
28
- import { GoalStatusBar, reconcileGoalStatusEntriesWithRuns, removeGoalStatusEntry, syncGoalStatusEntries, } from "./components/GoalStatusBar.js";
29
- import { Banner } from "./components/Banner.js";
30
- import { PlanOverlay } from "./components/PlanOverlay.js";
31
- import { ModelSelector } from "./components/ModelSelector.js";
32
- import { TaskOverlay } from "./components/TaskOverlay.js";
33
- import { GoalOverlay } from "./components/GoalOverlay.js";
34
- import { PixelOverlay } from "./components/PixelOverlay.js";
35
- import { SkillsOverlay } from "./components/SkillsOverlay.js";
36
- import { EyesOverlay } from "./components/EyesOverlay.js";
37
- import { ThemeSelector } from "./components/ThemeSelector.js";
38
- import { BackgroundTasksBar, getFooterStatusLayoutDecision, } from "./components/BackgroundTasksBar.js";
19
+ import { ChatScreen } from "./components/ChatScreen.js";
20
+ import { LIVE_TOOL_PANEL_ROWS } from "./components/LiveToolPanel.js";
21
+ import { FullScreenOverlayRouter } from "./components/FullScreenOverlayRouter.js";
22
+ import { SessionSummaryDisplay } from "./components/SessionSummary.js";
39
23
  import { useTheme, useSetTheme } from "./theme/theme.js";
40
24
  import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
41
25
  import { getGitBranch } from "../utils/git.js";
42
- import { getModel, getContextWindow, getMaxThinkingLevel } from "../core/model-registry.js";
26
+ import { getModel } from "../core/model-registry.js";
43
27
  import { SessionManager } from "../core/session-manager.js";
44
- import { appendMessagesToSession as appendSessionMessages, createCompactedSessionCheckpoint, } from "../core/session-compaction.js";
45
28
  import { log } from "../core/logger.js";
46
29
  import { getPendingUpdate, startPeriodicUpdateCheck, stopPeriodicUpdateCheck, } from "../core/auto-update.js";
47
30
  import { generateSessionTitle } from "../utils/session-title.js";
48
31
  import { SettingsManager } from "../core/settings-manager.js";
49
- import { shouldCompact, compact, getCompactionReserveTokens, } from "../core/compaction/compactor.js";
50
- import { estimateConversationTokens } from "../core/compaction/token-estimator.js";
51
32
  import { PROMPT_COMMANDS, getPromptCommand } from "../core/prompt-commands.js";
52
33
  import { isFirstTimeSetup, markSetupAudited, getAnnouncedLanguages, markLanguagesAnnounced, } from "../core/setup-history.js";
53
34
  import { loadCustomCommands } from "../core/custom-commands.js";
54
- import { buildSystemPrompt } from "../system-prompt.js";
55
- import { detectLanguages, LANGUAGE_DISPLAY_NAMES, } from "../core/language-detector.js";
35
+ import { detectLanguages } from "../core/language-detector.js";
56
36
  import { detectVerifyCommands } from "../core/verify-commands.js";
57
- import { FOCUSED_REPO_MAP_MAX_CHARS, FIRST_TURN_REPO_MAP_MAX_CHARS, buildRepoMap, createRepoMapCache, } from "../core/repomap.js";
58
- import { getLatestUserText, injectRepoMapContextMessages, stripRepoMapContextMessages, } from "../core/repomap-context.js";
37
+ import { RewindOverlay } from "./components/RewindOverlay.js";
38
+ import { reconcileGoalStatusEntriesWithRuns, removeGoalStatusEntry, syncGoalStatusEntries, } from "./components/GoalStatusBar.js";
59
39
  import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
60
40
  import { getMCPServers } from "../core/mcp/index.js";
61
41
  import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
62
- import { appendGoalDecision, appendGoalEvidence, formatGoalBlockingPrerequisites, goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns, summarizeGoalCounts, summarizeGoalCountsFromRuns, updateGoalTask, upsertGoalRun, } from "../core/goal-store.js";
63
- import { canCompleteGoalRun, decideGoalNextAction, shouldCreateVerifierFixTask, } from "../core/goal-controller.js";
42
+ import { splitAssistantStreamingText } from "./utils/assistant-stream-split.js";
43
+ import { getNextPendingTask, markTaskInProgress } from "../core/tasks-store.js";
44
+ import { buildUserContentWithAttachments } from "./prompt-routing.js";
45
+ import { submitPromptCommand } from "./submit-prompt-command.js";
46
+ import { handleUiSlashCommand } from "./submit-slash-commands.js";
47
+ import { buildIdealReviewMessage, evaluateIdealReview } from "../core/ideal-review.js";
48
+ import { buildLoopBreakMessage, evaluateLoopBreak } from "../core/loop-breaker.js";
49
+ import { buildRegroundingMessage } from "../core/regrounding.js";
50
+ import { getNextThinkingLevel, isThinkingLevelSupported } from "./thinking-level.js";
51
+ import { getDoneFlushDecision, shouldTopSpaceAfterPrintedAgentBoundary, shouldTopSpaceStreamingAssistant, } from "./layout-decisions.js";
52
+ import { isTranscriptSpacingItem } from "./transcript/spacing.js";
53
+ import { buildTranscriptLines } from "./transcript/transcript-lines.js";
54
+ import { useTranscriptScroll } from "./hooks/useTranscriptScroll.js";
55
+ import { scrollTranscriptByLines } from "./stores/transcript-scroll-store.js";
56
+ import { renderTranscriptItem } from "./transcript/TranscriptRenderer.js";
57
+ import { formatDuration } from "./duration-format.js";
58
+ import { pickDurationVerb } from "./duration-summary.js";
59
+ import { toErrorItem } from "./error-item.js";
60
+ import { appendGoalDecision, appendGoalEvidence, createGoalTask, formatGoalBlockingPrerequisites, goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns, updateGoalTask, upsertGoalRun, } from "../core/goal-store.js";
61
+ import { canCompleteGoalRun, decideGoalNextAction, } from "../core/goal-controller.js";
62
+ import { runGoalPrerequisiteChecks } from "../core/goal-prerequisites.js";
64
63
  import { runGoalVerifierCommand } from "../core/goal-verifier.js";
65
64
  import { listGoalWorkers, startGoalWorker, stopGoalWorker, subscribeGoalWorkerCompletions, } from "../core/goal-worker.js";
66
- import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent, isGoalSyntheticEvent, parseGoalSyntheticEvent, } from "./goal-events.js";
67
- /** Where ezcoder bugs should be reported. Surfaced in the guidance line. */
68
- const GGCODER_BUG_REPORT_URL = "github.com/Gahroot/ezcoder/issues";
69
- /**
70
- * Build an ErrorItem from any thrown value. Centralises headline / message /
71
- * guidance extraction so every error answers the same question for the user:
72
- * "Should I retry, or is this a ezcoder bug to report?"
73
- */
74
- function toErrorItem(err, id, contextPrefix) {
75
- const f = formatError(err);
76
- const headline = contextPrefix ? `${contextPrefix} — ${f.headline}` : f.headline;
77
- // For ezcoder bugs, swap the generic "see /help" guidance for an actual URL
78
- // so users have a clear place to send the report.
79
- const guidance = f.source === "ezcoder"
80
- ? `This looks like a ezcoder bug — please send it to the dev at ${GGCODER_BUG_REPORT_URL}.`
81
- : f.guidance;
82
- // Mirror every user-visible error into ~/.ezcoder/debug.log so reports can be
83
- // diagnosed even after the terminal scrollback is gone.
84
- log("ERROR", "ui-error", headline, {
85
- source: f.source,
86
- message: f.message,
87
- ...(f.provider ? { provider: f.provider } : {}),
88
- ...(f.statusCode != null ? { statusCode: String(f.statusCode) } : {}),
89
- ...(f.requestId ? { requestId: f.requestId } : {}),
90
- ...(err instanceof Error && err.stack ? { stack: err.stack } : {}),
91
- });
92
- return {
93
- kind: "error",
94
- headline,
95
- message: f.message,
96
- guidance,
97
- id,
98
- };
99
- }
100
- export function routePromptCommandInput(input, promptCommands = PROMPT_COMMANDS, customCommands = []) {
101
- const trimmed = input.trim();
102
- if (!trimmed.startsWith("/"))
103
- return null;
104
- const parts = trimmed.slice(1).split(" ");
105
- const cmdName = parts[0];
106
- const cmdArgs = parts.slice(1).join(" ").trim();
107
- const builtinCmd = promptCommands.find((c) => c.name === cmdName || c.aliases.includes(cmdName));
108
- const customCmd = !builtinCmd ? customCommands.find((c) => c.name === cmdName) : undefined;
109
- const promptText = builtinCmd?.prompt ?? customCmd?.prompt;
110
- if (!promptText)
111
- return null;
112
- return {
113
- cmdName,
114
- cmdArgs,
115
- promptText,
116
- fullPrompt: cmdArgs ? `${promptText}\n\n## User Instructions\n\n${cmdArgs}` : promptText,
117
- };
118
- }
119
- export function buildUserContentWithAttachments(text, inputImages, modelSupportsImages) {
120
- if (inputImages.length === 0)
121
- return text;
122
- const parts = [];
123
- if (text) {
124
- parts.push({ type: "text", text });
125
- }
126
- for (const img of inputImages) {
127
- if (img.kind === "text") {
128
- parts.push({
129
- type: "text",
130
- text: `<file name="${img.fileName}">\n${img.data}\n</file>`,
131
- });
132
- }
133
- else if (modelSupportsImages) {
134
- parts.push({ type: "image", mediaType: img.mediaType, data: img.data });
135
- }
136
- else {
137
- // GLM models: save image to temp file and instruct model to use vision MCP tool
138
- const ext = img.mediaType.split("/")[1] ?? "png";
139
- const tmpPath = `/tmp/ezcoder-img-${Date.now()}.${ext}`;
140
- try {
141
- writeFileSync(tmpPath, Buffer.from(img.data, "base64"));
142
- parts.push({
143
- type: "text",
144
- text: `[User attached an image saved at: ${tmpPath} — use the image_analysis tool to view and analyze it]`,
145
- });
146
- }
147
- catch {
148
- parts.push({
149
- type: "text",
150
- text: `[User attached an image but it could not be saved for analysis]`,
151
- });
152
- }
153
- }
154
- }
155
- // If only text parts remain after stripping images, simplify to plain string
156
- return parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
157
- }
158
- /** Tools that get aggregated into a single compact group when concurrent. */
159
- const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
65
+ import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent, } from "./goal-events.js";
66
+ import { buildGoalFinalSummarySections, buildGoalSummaryRows, goalPassedDetail, } from "./goal-summary.js";
67
+ import { addLinesChanged, buildSessionSummary, createSessionStats, recordServerToolCall, recordToolEnd, recordTurnEnd, } from "./session-summary.js";
68
+ import { compactHistory, getNextGeneratedItemId, isActiveItem, isSameAssistantText, normalizeAssistantText, partitionCompleted, pinStreamingTextBeforeToolBoundary, removeItemsWithIds, uniqueItemsById, } from "./item-helpers.js";
69
+ import { IDEAL_HOOK_NOTICE_TEXT, LOOP_BREAK_NOTICE_TEXT, REGROUNDING_NOTICE_TEXT, lastVisibleTranscriptItem, } from "./app-items.js";
70
+ export { buildUserContentWithAttachments, routePromptCommandInput } from "./prompt-routing.js";
71
+ export { getNextThinkingLevel } from "./thinking-level.js";
72
+ export { getChatControlsLayoutDecision, getDoneFlushDecision, getScrollStabilizationDecision, getStaticHistoryKey, hasParagraphBreakLiveUserMessage, isTallLiveUserMessage, shouldHideHistoryForOverlayView, shouldHideStaticItemsForOverlayView, shouldStabilizeOverlayPaneRerender, shouldTopSpaceAfterPrintedAgentBoundary, shouldTopSpaceAssistantAfterToolBoundary, shouldTopSpaceStreamingAssistant, } from "./layout-decisions.js";
73
+ export { getNextGeneratedItemId, isActiveItem, partitionCompleted, pinStreamingTextBeforeToolBoundary, } from "./item-helpers.js";
74
+ /** Tools that get aggregated into a single compact group when possible. */
75
+ const AGGREGATABLE_TOOLS = new Set([
76
+ "read",
77
+ "grep",
78
+ "find",
79
+ "ls",
80
+ "mcp__kencode-search__searchCode",
81
+ "mcp__kencode-search__referenceSources",
82
+ "mcp__kencode-search__discoverRepos",
83
+ ]);
160
84
  const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
161
- /**
162
- * Cap memory by replacing old items with tiny tombstones. Ink's <Static>
163
- * tracks rendered items by array length — the array must never shrink, but
164
- * we can swap out heavy objects for lightweight `{ kind: "tombstone", id }`
165
- * entries so GC can reclaim the original data.
166
- */
167
- const MAX_LIVE_HISTORY = 200;
168
- function compactHistory(items) {
169
- if (items.length <= MAX_LIVE_HISTORY)
170
- return items;
171
- const cutoff = items.length - MAX_LIVE_HISTORY;
172
- const compacted = new Array(items.length);
173
- for (let i = 0; i < cutoff; i++) {
174
- const it = items[i];
175
- compacted[i] = it.kind === "tombstone" ? it : { kind: "tombstone", id: it.id };
176
- }
177
- for (let i = cutoff; i < items.length; i++) {
178
- compacted[i] = items[i];
179
- }
180
- return compacted;
181
- }
182
85
  function summarizeGoalCompletion(summary) {
183
- const lines = summary
184
- .split("\n")
185
- .map((line) => line.trim())
186
- .filter((line) => line.length > 0 && line !== "[agent_done]");
187
- const statusLine = lines.find((line) => /^Status:/i.test(line));
188
- const changedLine = lines.find((line) => /^(Changed|Implemented|Fixed|Added|Key findings|Full verifier)/i.test(line));
189
- const verificationLine = lines.find((line) => /^(Verification|Verified|Result):/i.test(line));
190
- return statusLine ?? changedLine ?? verificationLine ?? lines[0];
86
+ const trimmed = summary.trim();
87
+ if (!trimmed)
88
+ return undefined;
89
+ return trimmed.length > 400 ? `${trimmed.slice(0, 397)}...` : trimmed;
191
90
  }
192
91
  function formatGoalWorkerFinishedTitle(taskTitle, status) {
193
- return status === "done" ? `Done: ${taskTitle}` : `Failed: ${taskTitle}`;
92
+ return status === "done" ? `Worker finished: ${taskTitle}` : `Worker failed: ${taskTitle}`;
194
93
  }
195
- function countGoalTasksByStatus(tasks, status) {
196
- return tasks.filter((task) => task.status === status).length;
94
+ function goalCompletionReason(check) {
95
+ return check.reason;
96
+ }
97
+ function getGoalContinuationChoiceKey({ runId, decision, }) {
98
+ switch (decision.kind) {
99
+ case "create_task":
100
+ return `${runId}:create_task:${decision.title}`;
101
+ case "start_worker":
102
+ case "pause":
103
+ return `${runId}:${decision.kind}:${decision.task.id}:${decision.attempts}`;
104
+ case "run_verifier":
105
+ return `${runId}:run_verifier:${decision.command}`;
106
+ case "terminal":
107
+ return `${runId}:terminal:${decision.status}`;
108
+ case "blocked":
109
+ case "wait":
110
+ case "complete":
111
+ return `${runId}:${decision.kind}:${decision.reason}`;
112
+ }
197
113
  }
198
- function firstText(values) {
199
- return values.find((value) => value !== undefined && value.trim().length > 0)?.trim();
114
+ function goalTerminalRunIdFromItem(item) {
115
+ if (item.kind !== "goal_progress" || item.phase !== "terminal")
116
+ return undefined;
117
+ if (!item.id.startsWith("goal-terminal-"))
118
+ return undefined;
119
+ return item.id.slice("goal-terminal-".length);
200
120
  }
201
- function truncateGoalSummary(value, maxLength = 90) {
202
- const normalized = value.replace(/\s+/g, " ").trim();
203
- if (normalized.length <= maxLength)
204
- return normalized;
205
- return `${normalized.slice(0, maxLength - 1)}…`;
121
+ function goalProgressMatchesDraft(item, draft) {
122
+ return (item.title === draft.title &&
123
+ item.detail === draft.detail &&
124
+ item.status === draft.status &&
125
+ JSON.stringify(item.summaryRows ?? []) === JSON.stringify(draft.summaryRows ?? []) &&
126
+ JSON.stringify(item.summarySections ?? []) === JSON.stringify(draft.summarySections ?? []));
206
127
  }
207
- export function buildGoalSummaryRows(run) {
208
- const rows = [];
209
- const doneTasks = countGoalTasksByStatus(run.tasks, "done");
210
- const failedTasks = countGoalTasksByStatus(run.tasks, "failed");
211
- const blockedTasks = countGoalTasksByStatus(run.tasks, "blocked");
212
- const taskSuffix = [
213
- failedTasks > 0 ? `${failedTasks} failed` : undefined,
214
- blockedTasks > 0 ? `${blockedTasks} blocked` : undefined,
215
- ].filter((item) => item !== undefined);
216
- rows.push({
217
- label: "Tasks",
218
- value: run.tasks.length > 0 ? `${doneTasks}/${run.tasks.length} done` : "none",
219
- ...(taskSuffix.length > 0 ? { detail: taskSuffix.join(", ") } : {}),
220
- });
221
- const verifierResult = run.verifier?.lastResult;
222
- const verifierDetail = firstText([verifierResult?.outputPath, run.verifier?.command]);
223
- rows.push({
224
- label: "Verifier",
225
- value: verifierResult?.status ?? (run.verifier?.command ? "ready" : "missing"),
226
- ...(verifierDetail ? { detail: truncateGoalSummary(verifierDetail) } : {}),
227
- });
228
- const latestEvidence = run.evidence.at(-1);
229
- rows.push({
230
- label: "Evidence",
231
- value: `${run.evidence.length} recorded`,
232
- ...(latestEvidence
233
- ? { detail: truncateGoalSummary(latestEvidence.path ?? latestEvidence.label) }
234
- : {}),
128
+ export function completedItemsWithDurableGoalTerminalProgress(items, runs) {
129
+ const runIds = new Set(runs.map((run) => run.id));
130
+ const terminalByRun = new Map(runs
131
+ .map((run) => [run.id, formatGoalTerminalProgress(run)])
132
+ .filter((entry) => entry[1] !== null));
133
+ if (runIds.size === 0)
134
+ return items;
135
+ let changed = false;
136
+ const reconciled = items.map((item, index) => {
137
+ const runId = goalTerminalRunIdFromItem(item);
138
+ if (!runId || !runIds.has(runId))
139
+ return item;
140
+ const draft = terminalByRun.get(runId);
141
+ if (item.kind === "goal_progress" && draft && goalProgressMatchesDraft(item, draft))
142
+ return item;
143
+ changed = true;
144
+ return { kind: "tombstone", id: `tombstone-${item.id}-${index}` };
235
145
  });
236
- if (run.status === "blocked" || run.status === "paused" || run.blockers.length > 0) {
237
- rows.push({
238
- label: run.status === "paused" ? "Paused on" : "Blocked on",
239
- value: truncateGoalSummary(goalHasBlockingPrerequisites(run)
240
- ? formatGoalBlockingPrerequisites(run)
241
- : (run.blockers[0] ?? "manual review"), 110),
242
- });
243
- }
244
- else if (run.successCriteria.length > 0) {
245
- rows.push({
246
- label: "Criteria",
247
- value: `${run.successCriteria.length} checked`,
248
- detail: truncateGoalSummary(run.successCriteria[0] ?? "", 80),
249
- });
250
- }
251
- return rows.slice(0, 4);
146
+ return changed ? reconciled : items;
252
147
  }
253
148
  export function formatGoalTerminalProgress(run) {
254
149
  switch (run.status) {
@@ -257,8 +152,9 @@ export function formatGoalTerminalProgress(run) {
257
152
  kind: "goal_progress",
258
153
  phase: "terminal",
259
154
  title: `Goal passed: ${run.title}`,
260
- detail: "Verifier evidence is recorded; auto-continuation stopped.",
155
+ detail: goalPassedDetail(run),
261
156
  summaryRows: buildGoalSummaryRows(run),
157
+ summarySections: buildGoalFinalSummarySections(run),
262
158
  status: run.status,
263
159
  };
264
160
  case "failed":
@@ -297,221 +193,49 @@ export function formatGoalTerminalProgress(run) {
297
193
  return null;
298
194
  }
299
195
  }
300
- export function shouldHideHistoryForOverlayView(_isOverlayView, _isAgentRunning) {
301
- // Ink Static is append-only. Passing [] for overlay panes rewrites the Static
302
- // accumulator and can destroy scrollback when the pane closes. Keep history
303
- // mounted and let overlays render below it.
304
- return false;
305
- }
306
- export function shouldStabilizeOverlayPaneRerender({ overlayPane, isAgentRunning, }) {
307
- return isAgentRunning && (overlayPane === "goal" || overlayPane === "plan");
308
- }
309
- export function shouldHideStaticItemsForOverlayView({ shouldHideHistoryForOverlay, stabilizeOverlayPaneRerender, }) {
310
- return shouldHideHistoryForOverlay && !stabilizeOverlayPaneRerender;
311
- }
312
- export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, hasTallLiveUserMessage = false, }) {
313
- const shouldStabilize = isUserScrolled || hasTallLiveUserMessage;
314
- return {
315
- preserveStatic: shouldStabilize && hasNewOutput,
316
- autoFollow: !shouldStabilize,
317
- };
318
- }
319
- export function isTallLiveUserMessage(text, rows) {
320
- return text.split("\n").length > Math.max(8, Math.floor(rows * 0.6));
321
- }
322
- export function getStaticHistoryKey({ resizeKey }) {
323
- return `${resizeKey}`;
324
- }
325
- // flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
326
- /** Check whether an item is still active (running spinner, pending result). */
327
- function isActiveItem(item) {
328
- switch (item.kind) {
329
- case "tool_start":
330
- case "server_tool_start":
331
- case "compacting":
332
- return true;
333
- case "tool_group":
334
- return item.tools.some((t) => t.status === "running");
335
- case "subagent_group":
336
- return item.agents.some((a) => a.status === "running");
337
- default:
338
- return false;
339
- }
340
- }
341
- /**
342
- * Partition live items into completed (flushable to Static) and still-active.
343
- * Completed items precede active ones — we flush the longest contiguous prefix
344
- * of completed items to keep ordering stable.
345
- */
346
- function partitionCompleted(items) {
347
- // Find the first active item — everything before it is safe to flush
348
- const firstActiveIdx = items.findIndex(isActiveItem);
349
- if (firstActiveIdx === -1) {
350
- // All items are completed
351
- return { flushed: items, remaining: [] };
352
- }
353
- if (firstActiveIdx === 0) {
354
- return { flushed: [], remaining: items };
355
- }
356
- return {
357
- flushed: items.slice(0, firstActiveIdx),
358
- remaining: items.slice(firstActiveIdx),
359
- };
360
- }
361
- // ── Duration summary ─────────────────────────────────────
362
- function formatDuration(ms) {
363
- const totalSec = Math.round(ms / 1000);
364
- if (totalSec < 60)
365
- return `${totalSec}s`;
366
- const min = Math.floor(totalSec / 60);
367
- const sec = totalSec % 60;
368
- return sec > 0 ? `${min}m ${sec}s` : `${min}m`;
369
- }
370
- function pickDurationVerb(toolsUsed) {
371
- const has = (name) => toolsUsed.includes(name);
372
- const hasAny = (...names) => names.some(has);
373
- const writing = has("edit") || has("write");
374
- const reading = has("read") || has("grep") || has("find") || has("ls");
375
- // Multi-tool combos (most specific first)
376
- if (has("subagent") && writing)
377
- return "Orchestrated changes for";
378
- if (has("subagent"))
379
- return "Delegated work for";
380
- if (has("web-fetch") && writing)
381
- return "Researched & coded for";
382
- if (has("web-fetch") && reading)
383
- return "Researched for";
384
- if (has("web-fetch"))
385
- return "Fetched the web for";
386
- if (has("bash") && writing)
387
- return "Built & ran for";
388
- if (has("edit") && has("write"))
389
- return "Crafted code for";
390
- if (has("edit") && has("bash"))
391
- return "Refactored & tested for";
392
- if (has("edit") && reading)
393
- return "Refactored for";
394
- if (has("edit"))
395
- return "Refactored for";
396
- if (has("write") && has("bash"))
397
- return "Wrote & ran for";
398
- if (has("write") && reading)
399
- return "Wrote code for";
400
- if (has("write"))
401
- return "Wrote code for";
402
- if (has("bash") && has("grep"))
403
- return "Hacked away for";
404
- if (has("bash") && reading)
405
- return "Ran & investigated for";
406
- if (has("bash"))
407
- return "Executed commands for";
408
- if (hasAny("tasks", "task-output", "task-stop"))
409
- return "Managed tasks for";
410
- if (has("grep") && has("read"))
411
- return "Investigated for";
412
- if (has("grep") && has("find"))
413
- return "Scoured the codebase for";
414
- if (has("grep"))
415
- return "Searched for";
416
- if (has("read") && has("find"))
417
- return "Explored for";
418
- if (has("read"))
419
- return "Studied the code for";
420
- if (has("find") || has("ls"))
421
- return "Browsed files for";
422
- // No tools used — pure text response
423
- const phrases = [
424
- "Pondered for",
425
- "Thought for",
426
- "Reasoned for",
427
- "Mulled it over for",
428
- "Noodled on it for",
429
- "Brewed up a response in",
430
- "Cooked up an answer in",
431
- "Worked out a reply in",
432
- "Channeled wisdom for",
433
- "Conjured a response in",
434
- ];
435
- return phrases[Math.floor(Math.random() * phrases.length)];
436
- }
437
- // ── Animated thinking border ────────────────────────────────
438
- const THINKING_BORDER_COLORS = ["#60a5fa", "#818cf8", "#a78bfa", "#818cf8", "#60a5fa"];
439
- // ── Task count helper ───────────────────────────────────────
440
- function getTaskCount(cwd) {
441
- try {
442
- const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
443
- const data = readFileSync(join(homedir(), ".ezcoder-tasks", "projects", hash, "tasks.json"), "utf-8");
444
- const tasks = JSON.parse(data);
445
- return tasks.filter((t) => t.status !== "done").length;
446
- }
447
- catch {
448
- return 0;
449
- }
450
- }
451
- function getNextPendingTask(cwd) {
452
- try {
453
- const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
454
- const data = readFileSync(join(homedir(), ".ezcoder-tasks", "projects", hash, "tasks.json"), "utf-8");
455
- const tasks = JSON.parse(data);
456
- const pending = tasks.find((t) => t.status === "pending");
457
- if (!pending)
458
- return null;
459
- return {
460
- id: pending.id,
461
- title: pending.title,
462
- prompt: pending.prompt || pending.text || pending.title,
463
- };
464
- }
465
- catch {
466
- return null;
467
- }
468
- }
469
- function markTaskInProgress(cwd, taskId) {
470
- try {
471
- const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
472
- const filePath = join(homedir(), ".ezcoder-tasks", "projects", hash, "tasks.json");
473
- const data = readFileSync(filePath, "utf-8");
474
- const tasks = JSON.parse(data);
475
- const updated = tasks.map((t) => (t.id === taskId ? { ...t, status: "in-progress" } : t));
476
- writeFileSync(filePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
477
- }
478
- catch {
479
- // ignore
480
- }
196
+ export function nextGoalModeAfterAgentDone({ currentMode, runningGoalIds, queuedSyntheticEvents, activeContinuationFlights = 0, wasGoalSetupTurn, }) {
197
+ if (wasGoalSetupTurn)
198
+ return "off";
199
+ if (currentMode === "planner" || currentMode === "setup")
200
+ return currentMode;
201
+ if (queuedSyntheticEvents > 0)
202
+ return "coordinator";
203
+ if (activeContinuationFlights > 0)
204
+ return "coordinator";
205
+ if (currentMode === "coordinator" && runningGoalIds > 0)
206
+ return "coordinator";
207
+ return "off";
481
208
  }
209
+ // ── App Component ──────────────────────────────────────────
482
210
  /**
483
- * Mark a task as done in the on-disk tasks.json. Used by run-all auto-advance
484
- * so the in-flight task is recorded as completed even when the agent forgets
485
- * to call the `tasks` tool. Errors are swallowed — if the file is missing or
486
- * the id is stale, the next auto-advance step still proceeds correctly.
211
+ * Extract inline image previews carried on a tool result's `details` payload.
212
+ * Image-producing tools (e.g. screenshot) attach `{ imagePreviews: [...] }` so
213
+ * the terminal-history printer can render the captured pixels inline.
487
214
  */
488
- function markTaskDone(cwd, taskId) {
489
- try {
490
- const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
491
- const filePath = join(homedir(), ".ezcoder-tasks", "projects", hash, "tasks.json");
492
- const data = readFileSync(filePath, "utf-8");
493
- const tasks = JSON.parse(data);
494
- const updated = tasks.map((t) => (t.id === taskId ? { ...t, status: "done" } : t));
495
- writeFileSync(filePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
496
- }
497
- catch {
498
- // ignore
499
- }
215
+ function extractToolImagePreviews(details) {
216
+ if (!details || typeof details !== "object")
217
+ return undefined;
218
+ const previews = details.imagePreviews;
219
+ if (!Array.isArray(previews))
220
+ return undefined;
221
+ const valid = previews.filter((p) => typeof p === "object" &&
222
+ p !== null &&
223
+ typeof p.base64 === "string" &&
224
+ typeof p.mediaType === "string");
225
+ return valid.length > 0 ? valid : undefined;
500
226
  }
501
- // ── App Component ──────────────────────────────────────────
502
227
  export function App(props) {
503
228
  const theme = useTheme();
504
229
  const switchTheme = useSetTheme();
505
- const { columns, resizeKey } = useTerminalSize();
230
+ const { write: writeStdout } = useStdout();
231
+ const { columns, rows } = useTerminalSize();
506
232
  // Hoisted before terminal title hook so it can reference them
507
233
  const [lastUserMessage, setLastUserMessage] = useState("");
234
+ // Bumped on every prompt submit; the fullscreen transcript scroll controller
235
+ // watches this to snap back to the bottom so the newest output is visible.
236
+ const [scrollResetToken, setScrollResetToken] = useState(0);
508
237
  const [exitPending, setExitPending] = useState(false);
509
- // Initialize from planModeRef (lives outside React in cli.ts) so plan
510
- // mode survives /clear's unmount/remount, matching the prior behavior
511
- // where /clear didn't toggle plan mode off.
512
- const [planMode, setPlanMode] = useState(props.planModeRef?.current ?? false);
513
- const planModeLocalRef = useRef(false);
514
- planModeLocalRef.current = planMode;
238
+ const [quittingSummary, setQuittingSummary] = useState(null);
515
239
  // Terminal title — updated later after agentLoop is created
516
240
  // (hoisted here so the hook is always called in the same order)
517
241
  const [titleRunning, setTitleRunning] = useState(false);
@@ -521,14 +245,11 @@ export function App(props) {
521
245
  isRunning: titleRunning,
522
246
  sessionTitle,
523
247
  });
524
- // Items scrolled into Static (history). For restored sessions, seed the
525
- // initial array directly matches how every other Ink chat agent passes
526
- // messages to <Static> (cat-code, harness, p90-cli, openai-chatgpt, lms,
527
- // gatsby). Ink's Static (build/components/Static.js) starts with index=0
528
- // so slice(0) returns the full array regardless of length.
248
+ // Completed transcript rows are kept as durable session data but are no longer
249
+ // rendered through Ink history. They are serialized once into real terminal
250
+ // scrollback via terminalHistoryPrinter, while Ink owns only live rows and
251
+ // controls. This avoids Static/log-update replay drift on resize/remount.
529
252
  const [history, setHistory] = useState(() => {
530
- // sessionStore wins (lives across remount). Falls back to initialHistory
531
- // (loaded from a session file at startup), then a fresh banner-only list.
532
253
  const stored = props.sessionStore?.history;
533
254
  if (stored && stored.length > 0)
534
255
  return stored;
@@ -538,57 +259,68 @@ export function App(props) {
538
259
  return [{ kind: "banner", id: "banner" }];
539
260
  });
540
261
  // Items from the current/last turn — rendered in the live area so they stay visible.
541
- // Seed from sessionStore so Goal progress/completion rows and other live output
542
- // survive pane/overlay/resize remounts before they are flushed to <Static>.
543
- const [liveItems, setLiveItems] = useState(() => props.sessionStore?.liveItems ?? []);
262
+ // Seed from sessionStore so live output
263
+ // survives pane/overlay/resize remounts before it is finalized.
264
+ const [liveItems, setLiveItems] = useState(() => {
265
+ const restoredLiveItems = uniqueItemsById(props.sessionStore?.liveItems ?? []);
266
+ const restoredHistoryIds = new Set(history.map((item) => item.id));
267
+ return removeItemsWithIds(restoredLiveItems, restoredHistoryIds);
268
+ });
269
+ // Rolling feed of recent tool actions for the pinned LiveToolPanel. Kept
270
+ // separate from `liveItems` (the scrollback record) so tool calls mutate in
271
+ // place above the activity bar instead of spamming the transcript.
272
+ const [liveToolFeed, setLiveToolFeed] = useState([]);
544
273
  // overlay seeded from sessionStore (lives across remount). Falls back to
545
274
  // props.initialOverlay (CLI launched with one), then null.
546
275
  const [overlay, setOverlay] = useState(props.sessionStore?.overlay ?? props.initialOverlay ?? null);
547
- const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
548
- const [goalCount, setGoalCount] = useState(0);
549
- const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
550
- const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
551
276
  const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
552
- // Seed from sessionStore so "Run All" chaining survives the resetUI()
553
- // remount that startTask() triggers between tasks.
277
+ // Signal that pushes text into the InputArea composer (e.g. restoring queued
278
+ // messages after an interrupt). Bumping `nonce` triggers the injection even
279
+ // when the text is identical to a prior restore.
280
+ const [composerInject, setComposerInject] = useState(null);
281
+ const agentRunningRef = useRef(false);
554
282
  const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
555
283
  const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
556
- // Task id of the run-all task currently driving the agent. Tracked so the
557
- // run-all `onDone` auto-advance can mark the just-finished task as done
558
- // even when the agent forgets to call the tasks tool. Mirrored through
559
- // sessionStore so the value survives the resetUI() remount between tasks.
560
- const currentTaskIdRef = useRef(props.sessionStore?.currentTaskId ?? null);
561
284
  const startTaskRef = useRef(() => { });
562
- const agentRunningRef = useRef(false);
563
- const runningGoalIdsRef = useRef(new Set());
564
- const activeVerifierRunIdsRef = useRef(new Set());
565
- const startGoalRunRef = useRef(() => { });
566
285
  const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
567
286
  const currentPixelFixRef = useRef(null);
568
287
  const startPixelFixRef = useRef(() => { });
569
288
  const cwdRef = useRef(props.cwd);
570
289
  const [displayedCwd, setDisplayedCwd] = useState(props.cwd);
290
+ // /rewind overlay: holds the checkpoint list while the picker is open.
291
+ const [rewindCheckpoints, setRewindCheckpoints] = useState(null);
292
+ // Monotonic user-turn counter keying per-turn checkpoints.
293
+ const rewindTurnRef = useRef(0);
294
+ const taskPicker = useTaskPickerController({
295
+ displayedCwd,
296
+ onStartTask: (title, prompt, taskId) => startTaskRef.current(title, prompt, taskId),
297
+ onRunAllTasksChange: setRunAllTasks,
298
+ });
571
299
  const [doneStatus, setDoneStatus] = useState(props.sessionStore?.doneStatus ?? null);
572
300
  // Suppress "done" status when a plan overlay is about to open
573
301
  const planOverlayPendingRef = useRef(false);
574
302
  const [gitBranch, setGitBranch] = useState(null);
575
303
  const [currentModel, setCurrentModel] = useState(props.model);
304
+ const currentModelRef = useRef(props.model);
576
305
  const [currentProvider, setCurrentProvider] = useState(props.provider);
306
+ const currentProviderRef = useRef(props.provider);
577
307
  const [currentTools, setCurrentTools] = useState(props.tools);
578
308
  const currentToolsRef = useRef(props.tools);
579
- const [thinkingEnabled, setThinkingEnabled] = useState(!!props.thinking);
309
+ const [thinkingLevel, setThinkingLevel] = useState(props.thinking);
310
+ const [renderMarkdown, setRenderMarkdown] = useState(true);
580
311
  const messagesRef = useRef(props.sessionStore?.messages ?? props.messages);
581
- const repoMapInjectionEnabledRef = useRef(true);
582
- const repoMapDirtyRef = useRef(true);
583
- const repoMapMarkdownRef = useRef("");
584
- const repoMapSnapshotRef = useRef(undefined);
585
- const repoMapChangedCountRef = useRef(0);
586
- const repoMapCacheRef = useRef(createRepoMapCache());
587
312
  const [planAutoExpand, setPlanAutoExpand] = useState(props.sessionStore?.planAutoExpand ?? false);
313
+ const [goalAutoExpand, setGoalAutoExpand] = useState(props.sessionStore?.goalAutoExpand ?? false);
314
+ const goalAutoExpandRef = useRef(props.sessionStore?.goalAutoExpand ?? false);
315
+ const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
316
+ const runningGoalIdsRef = useRef(new Set());
317
+ const activeVerifierRunIdsRef = useRef(new Set());
318
+ const goalContinuationFlightsRef = useRef(new Set());
319
+ const goalContinuationRecentChoicesRef = useRef(new Map());
320
+ const startGoalRunRef = useRef(() => { });
588
321
  const approvedPlanPathRef = useRef(props.sessionStore?.approvedPlanPath);
589
322
  const planStepsRef = useRef(props.sessionStore?.planSteps ?? []);
590
323
  const [planSteps, setPlanSteps] = useState(props.sessionStore?.planSteps ?? []);
591
- const planModeStateRef = useRef(planMode);
592
324
  // Stuck-guard for the plan-continuation follow-up nudge. Tracks how many
593
325
  // times we've nudged the agent to continue the same step. Reset whenever a
594
326
  // new [DONE:n] marker advances progress (see onTurnText). Caps at 2 nudges
@@ -596,32 +328,25 @@ export function App(props) {
596
328
  const followUpNudgesRef = useRef({ step: 0, count: 0 });
597
329
  // Seed the per-item ID counter so it doesn't collide with IDs already in
598
330
  // sessionStore.history (which survives remount). Without this, a remount
599
- // (resize, overlay toggle, etc.) starts the counter at 0 and new items
600
- // generate ids "0", "1", "2"… that collide with the same ids from the
601
- // previous mount, triggering React's duplicate-key warning and causing
602
- // duplicate/omitted renders.
603
- const nextIdRef = useRef((() => {
604
- const items = [
605
- ...(props.sessionStore?.history ?? props.initialHistory ?? []),
606
- ...(props.sessionStore?.liveItems ?? []),
607
- ];
608
- let max = -1;
609
- for (const item of items) {
610
- const n = Number(item.id);
611
- if (Number.isFinite(n) && n > max)
612
- max = n;
613
- }
614
- return max + 1;
615
- })());
331
+ // (resize, overlay toggle, task pane open, etc.) starts the counter at 0
332
+ // and new items generate ids "ui-0", "ui-1", "ui-2"… that collide with
333
+ // the same ids from the previous mount, triggering React's duplicate-key
334
+ // warning and causing duplicate/omitted renders.
335
+ const nextIdRef = useRef(getNextGeneratedItemId([
336
+ ...(props.sessionStore?.history ?? props.initialHistory ?? []),
337
+ ...(props.sessionStore?.liveItems ?? []),
338
+ ]));
616
339
  const sessionManagerRef = useRef(props.sessionsDir ? new SessionManager(props.sessionsDir) : null);
617
340
  const sessionPathRef = useRef(props.sessionStore?.sessionPath ?? props.sessionPath);
618
341
  const persistedIndexRef = useRef(messagesRef.current.length);
342
+ const sessionStatsRef = useRef(props.sessionStore?.sessionStats ??
343
+ createSessionStats({ sessionId: props.sessionStore?.sessionId ?? props.sessionId }));
344
+ const [idealReviewEnabled, setIdealReviewEnabled] = useState(props.sessionStore?.idealReviewEnabled ?? props.idealReviewEnabled ?? true);
345
+ const idealReviewEnabledRef = useRef(idealReviewEnabled);
619
346
  /** Last actual API-reported input token count (from turn_end). */
620
347
  const lastActualTokensRef = useRef(0);
621
348
  /** Timestamp (ms) when lastActualTokensRef was last updated by turn_end. */
622
349
  const lastActualTokensTimestampRef = useRef(0);
623
- /** Timestamp of last compaction — used for time-based cooldown and staleness detection. */
624
- const lastCompactionTimeRef = useRef(0);
625
350
  /**
626
351
  * Languages whose style packs are currently injected into the system prompt.
627
352
  * Grown by `maybeInjectLanguagePacks` after `write`/`bash` tool results when
@@ -643,6 +368,29 @@ export function App(props) {
643
368
  */
644
369
  const triggerAutoSetupRef = useRef(async () => { });
645
370
  const getId = () => `ui-${nextIdRef.current++}`;
371
+ useEffect(() => {
372
+ idealReviewEnabledRef.current = idealReviewEnabled;
373
+ if (props.sessionStore)
374
+ props.sessionStore.idealReviewEnabled = idealReviewEnabled;
375
+ }, [idealReviewEnabled, props.sessionStore]);
376
+ const sessionStore = props.sessionStore;
377
+ const { planMode, goalMode, rebuildSystemPrompt, replaceSystemPrompt, setPlanModeAndPrompt, setGoalModeAndPrompt, } = useModeState({
378
+ initialPlanMode: props.sessionStore?.planMode ?? props.planModeRef?.current ?? false,
379
+ initialGoalMode: props.sessionStore?.goalMode ?? props.goalModeRef?.current ?? "off",
380
+ skills: props.skills,
381
+ planModeRef: props.planModeRef,
382
+ goalModeRef: props.goalModeRef,
383
+ sessionStore: props.sessionStore,
384
+ cwdRef,
385
+ currentToolsRef,
386
+ providerRef: currentProviderRef,
387
+ approvedPlanPathRef,
388
+ injectedLanguagesRef,
389
+ messagesRef,
390
+ });
391
+ const appendGoalAgentTransition = useCallback((text) => {
392
+ setLiveItems((prev) => [...prev, { kind: "goal_agent_transition", text, id: getId() }]);
393
+ }, []);
646
394
  const appendGoalProgress = useCallback((item) => {
647
395
  setLiveItems((prev) => [...prev, { ...item, id: getId() }]);
648
396
  }, []);
@@ -663,47 +411,64 @@ export function App(props) {
663
411
  return next;
664
412
  });
665
413
  }, [props.sessionStore]);
666
- // Two-phase flush: items waiting to be moved to Static history after the
667
- // live area has been cleared and Ink has committed the smaller output.
668
- const pendingFlushRef = useRef([]);
669
- const [flushGeneration, setFlushGeneration] = useState(0);
670
- /** Queue items for two-phase flush and signal the drain effect. */
671
- const queueFlush = useCallback((items) => {
672
- if (items.length === 0)
673
- return;
674
- pendingFlushRef.current = [...pendingFlushRef.current, ...items];
675
- if (props.sessionStore) {
676
- const queuedIds = new Set(items.map((item) => item.id));
677
- props.sessionStore.liveItems = (props.sessionStore.liveItems ?? []).filter((item) => !queuedIds.has(item.id));
678
- }
679
- setFlushGeneration((g) => g + 1);
680
- }, [props.sessionStore]);
414
+ const { pendingHistoryFlushRef, streamedAssistantFlushRef, queueFlush, finalizeSubmittedUserItem, clearPendingHistory, } = useTranscriptHistory({
415
+ // In fullscreen alt-screen mode the transcript renders inside Ink (the
416
+ // viewport), so we must NOT write completed rows to native scrollback —
417
+ // doing so would scroll the live frame. Dropping the printer keeps history
418
+ // accumulating in React state (still flushed + persisted) without any
419
+ // stdout writes. The legacy scrollback path keeps the printer.
420
+ terminalHistoryPrinter: props.fullscreen ? undefined : props.terminalHistoryPrinter,
421
+ terminalHistoryContext: {
422
+ theme,
423
+ columns,
424
+ version: props.version,
425
+ model: currentModel,
426
+ provider: currentProvider,
427
+ cwd: displayedCwd,
428
+ },
429
+ writeStdout,
430
+ sessionPathRef,
431
+ sessionManagerRef,
432
+ sessionStore,
433
+ history,
434
+ setHistory,
435
+ setLiveItems,
436
+ });
681
437
  // Mirror runtime state choices (model/provider/thinking) into renderApp's
682
438
  // closure so unmount/remount preserves them.
683
439
  const onRuntimeStateChange = props.onRuntimeStateChange;
684
440
  useEffect(() => {
441
+ currentModelRef.current = currentModel;
685
442
  onRuntimeStateChange?.({ model: currentModel });
686
443
  }, [currentModel, onRuntimeStateChange]);
687
444
  useEffect(() => {
688
445
  onRuntimeStateChange?.({ provider: currentProvider });
689
446
  }, [currentProvider, onRuntimeStateChange]);
447
+ useEffect(() => {
448
+ if (thinkingLevel && !isThinkingLevelSupported(currentProvider, currentModel, thinkingLevel)) {
449
+ setThinkingLevel(getNextThinkingLevel(currentProvider, currentModel, undefined));
450
+ }
451
+ }, [currentProvider, currentModel, thinkingLevel]);
690
452
  useEffect(() => {
691
453
  onRuntimeStateChange?.({
692
- thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
454
+ thinking: thinkingLevel,
693
455
  });
694
- }, [thinkingEnabled, currentModel, onRuntimeStateChange]);
456
+ }, [thinkingLevel, onRuntimeStateChange]);
695
457
  // Mirror session state into renderApp's closure so resetUI() can re-seed
696
458
  // the conversation on remount. Each panel that previously did a bare ANSI
697
- // screen clear (overlay open/close, plan accept/reject, /clear, startTask)
459
+ // screen clear (overlay open/close, plan accept/reject, /clear)
698
460
  // now goes through resetUI; without these mirrors, the chat would vanish.
699
- const sessionStore = props.sessionStore;
461
+ const historyRef = useRef(history);
700
462
  useEffect(() => {
463
+ historyRef.current = history;
701
464
  if (sessionStore)
702
465
  sessionStore.history = history;
703
466
  }, [history, sessionStore]);
704
467
  useEffect(() => {
705
- if (sessionStore)
706
- sessionStore.liveItems = liveItems;
468
+ if (!sessionStore)
469
+ return;
470
+ const historyIds = new Set(historyRef.current.map((item) => item.id));
471
+ sessionStore.liveItems = removeItemsWithIds(uniqueItemsById(liveItems), historyIds);
707
472
  }, [liveItems, sessionStore]);
708
473
  useEffect(() => {
709
474
  if (sessionStore)
@@ -721,10 +486,27 @@ export function App(props) {
721
486
  if (sessionStore)
722
487
  sessionStore.overlay = overlay;
723
488
  }, [overlay, sessionStore]);
489
+ useEffect(() => {
490
+ goalAutoExpandRef.current = goalAutoExpand;
491
+ if (sessionStore)
492
+ sessionStore.goalAutoExpand = goalAutoExpand;
493
+ }, [goalAutoExpand, sessionStore]);
724
494
  useEffect(() => {
725
495
  if (sessionStore)
726
496
  sessionStore.goalStatusEntries = goalStatusEntries;
727
497
  }, [goalStatusEntries, sessionStore]);
498
+ useEffect(() => {
499
+ if (sessionStore)
500
+ sessionStore.planMode = planMode;
501
+ }, [planMode, sessionStore]);
502
+ useEffect(() => {
503
+ if (sessionStore)
504
+ sessionStore.goalMode = goalMode;
505
+ }, [goalMode, sessionStore]);
506
+ useEffect(() => {
507
+ if (sessionStore)
508
+ sessionStore.sessionStats = sessionStatsRef.current;
509
+ }, [sessionStore]);
728
510
  // pendingAction is consumed via a useEffect AFTER agentLoop is created
729
511
  // — see below where useAgentLoop is set up.
730
512
  const pendingActionConsumedRef = useRef(false);
@@ -742,14 +524,13 @@ export function App(props) {
742
524
  }, [displayedCwd]);
743
525
  useEffect(() => {
744
526
  let cancelled = false;
745
- const refreshGoalCount = () => {
527
+ const refreshGoalState = () => {
746
528
  void reconcileActiveGoalRuns(props.cwd, {
747
529
  isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
748
530
  }).then(({ runs }) => {
749
- const counts = summarizeGoalCountsFromRuns(runs);
750
531
  if (cancelled)
751
532
  return;
752
- setGoalCount(counts.active);
533
+ setHistory((prev) => completedItemsWithDurableGoalTerminalProgress(prev, runs));
753
534
  setGoalStatusEntries((prev) => {
754
535
  const next = reconcileGoalStatusEntriesWithRuns(prev, runs, {
755
536
  isWorkerActive: (workerId, run) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId &&
@@ -763,13 +544,13 @@ export function App(props) {
763
544
  });
764
545
  });
765
546
  };
766
- refreshGoalCount();
767
- const interval = setInterval(refreshGoalCount, 1000);
547
+ refreshGoalState();
548
+ const interval = setInterval(refreshGoalState, 1_000);
768
549
  return () => {
769
550
  cancelled = true;
770
551
  clearInterval(interval);
771
552
  };
772
- }, [props.cwd]);
553
+ }, [props.cwd, props.sessionStore]);
773
554
  // Periodic update check during long sessions
774
555
  useEffect(() => {
775
556
  startPeriodicUpdateCheck(props.version, (msg) => {
@@ -789,27 +570,29 @@ export function App(props) {
789
570
  useEffect(() => {
790
571
  currentToolsRef.current = currentTools;
791
572
  }, [currentTools]);
792
- // ── Plan mode wiring ─────────────────────────────────────
793
- // Sync planModeRef with React state
794
573
  useEffect(() => {
795
- planModeStateRef.current = planMode;
796
- if (props.planModeRef) {
797
- props.planModeRef.current = planMode;
798
- }
799
- }, [planMode, props.planModeRef]);
800
- const rebuildSystemPrompt = useCallback(async (options) => {
801
- const approvedPlanPath = options?.clearApprovedPlan
802
- ? undefined
803
- : (options?.approvedPlanPath ?? approvedPlanPathRef.current);
804
- return buildSystemPrompt(options?.cwd ?? cwdRef.current, props.skills, options?.planMode ?? planModeStateRef.current, approvedPlanPath, (options?.tools ?? currentToolsRef.current).map((tool) => tool.name), options?.activeLanguages ?? injectedLanguagesRef.current);
805
- }, [props.skills]);
806
- const replaceSystemPrompt = useCallback(async (options) => {
807
- const newPrompt = await rebuildSystemPrompt(options);
808
- if (messagesRef.current[0]?.role === "system") {
809
- messagesRef.current[0] = { role: "system", content: newPrompt };
810
- }
811
- return newPrompt;
812
- }, [rebuildSystemPrompt]);
574
+ if (!props.connectInitialMcpTools)
575
+ return;
576
+ let cancelled = false;
577
+ void props
578
+ .connectInitialMcpTools()
579
+ .then((mcpTools) => {
580
+ if (cancelled || mcpTools.length === 0)
581
+ return;
582
+ setCurrentTools((prev) => {
583
+ const next = [...prev.filter((tool) => !tool.name.startsWith("mcp__")), ...mcpTools];
584
+ currentToolsRef.current = next;
585
+ void replaceSystemPrompt({ tools: next });
586
+ return next;
587
+ });
588
+ })
589
+ .catch((err) => {
590
+ log("WARN", "mcp", `MCP initialization failed: ${err instanceof Error ? err.message : String(err)}`);
591
+ });
592
+ return () => {
593
+ cancelled = true;
594
+ };
595
+ }, [props.connectInitialMcpTools, replaceSystemPrompt]);
813
596
  /**
814
597
  * Unified "apply detection result" pipeline. Called from three sites:
815
598
  * 1. Initial mount (existing project at startup).
@@ -894,89 +677,17 @@ export function App(props) {
894
677
  useEffect(() => {
895
678
  void applyLanguageDetectionRef.current("initial");
896
679
  }, []);
897
- // Rebuild system prompt when plan mode changes
898
- useEffect(() => {
899
- void replaceSystemPrompt({ planMode });
900
- }, [planMode, replaceSystemPrompt]);
901
- // Wire onEnterPlan callback ref
902
- useEffect(() => {
903
- if (props.onEnterPlanRef) {
904
- props.onEnterPlanRef.current = (reason) => {
905
- setPlanMode(true);
906
- const msg = reason ? `Plan Mode Activated — ${reason}` : "Plan Mode Activated";
907
- setLiveItems((prev) => [
908
- ...prev,
909
- { kind: "plan_transition", text: msg, active: true, id: getId() },
910
- ]);
911
- };
912
- }
913
- }, [props.onEnterPlanRef]);
914
- // Wire onExitPlan callback ref
915
- useEffect(() => {
916
- if (props.onExitPlanRef) {
917
- props.onExitPlanRef.current = async (planPath) => {
918
- // Deactivate plan mode, store approved plan path, open pane
919
- planModeStateRef.current = false;
920
- setPlanMode(false);
921
- approvedPlanPathRef.current = planPath;
922
- await replaceSystemPrompt({ planMode: false, approvedPlanPath: planPath });
923
- // Use setTimeout to open pane after the current tool execution completes,
924
- // so the turn can finish and the UI transitions cleanly
925
- // Flag that the plan overlay is about to open — suppresses the
926
- // premature "done" status that fires when the agent loop finishes
927
- planOverlayPendingRef.current = true;
928
- setTimeout(() => {
929
- setPlanAutoExpand(true);
930
- setOverlay("plan");
931
- // Don't clear planOverlayPendingRef here — keep it true until
932
- // the user actually approves/rejects the plan. Clearing it on a
933
- // timer causes a race where agent_done fires after the 300ms
934
- // timeout but before the user interacts, triggering a premature
935
- // completion sound.
936
- }, 300);
937
- return ("Plan submitted. Exiting plan mode.\n" +
938
- "The plan pane is opening for user review.\n" +
939
- "Plan saved at: " +
940
- planPath);
941
- };
942
- }
943
- }, [props.onExitPlanRef, replaceSystemPrompt]);
944
- const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
945
- const sm = sessionManagerRef.current;
946
- if (!sm)
947
- return;
948
- await appendSessionMessages(sm, sessionPath, messages, startIndex);
949
- }, []);
950
- const persistCompactedSession = useCallback(async (compactedMessages) => {
951
- const sm = sessionManagerRef.current;
952
- if (!sm)
953
- return;
954
- const session = await createCompactedSessionCheckpoint(sm, {
955
- cwd: cwdRef.current,
956
- provider: currentProvider,
957
- model: currentModel,
958
- messages: compactedMessages,
959
- });
960
- sessionPathRef.current = session.path;
961
- persistedIndexRef.current = compactedMessages.length;
962
- if (sessionStore) {
963
- sessionStore.sessionPath = session.path;
964
- sessionStore.messages = [...compactedMessages];
965
- }
966
- log("INFO", "compaction", "Persisted compacted session checkpoint", { path: session.path });
967
- }, [currentModel, currentProvider, sessionStore]);
968
- const persistNewMessages = useCallback(async () => {
969
- const sp = sessionPathRef.current;
970
- if (!sp)
971
- return;
972
- const allMsgs = messagesRef.current;
973
- await appendMessagesToSession(sp, allMsgs, persistedIndexRef.current);
974
- persistedIndexRef.current = allMsgs.length;
975
- if (sessionStore) {
976
- sessionStore.messages = [...allMsgs];
977
- sessionStore.sessionPath = sp;
978
- }
979
- }, [appendMessagesToSession, sessionStore]);
680
+ const { persistCompactedSession, persistNewMessages } = useSessionPersistence({
681
+ sessionManagerRef,
682
+ sessionPathRef,
683
+ sessionStatsRef,
684
+ persistedIndexRef,
685
+ messagesRef,
686
+ cwdRef,
687
+ currentProvider,
688
+ currentModel,
689
+ sessionStore,
690
+ });
980
691
  /**
981
692
  * Run the language detector against the current cwd. If the detected set is a
982
693
  * strict superset of what's already injected, rebuild the system prompt with
@@ -987,7 +698,7 @@ export function App(props) {
987
698
  * Other tool kinds skip detection entirely to avoid wasted filesystem stats.
988
699
  *
989
700
  * No restart required: the system prompt is mutated in place, same mechanism
990
- * already used for plan mode + pixel-fix chdir.
701
+ * used for pixel-fix chdir.
991
702
  *
992
703
  * Stored in a ref so `onToolEnd` (whose useCallback dep array is intentionally
993
704
  * empty to keep agent-loop options stable) can call the freshest version.
@@ -1011,189 +722,25 @@ export function App(props) {
1011
722
  });
1012
723
  }
1013
724
  }, [props.settingsFile]);
1014
- const compactionAbortRef = useRef(null);
1015
- const compactConversation = useCallback(async (messages, signal) => {
1016
- const contextWindow = getContextWindow(currentModel, contextWindowOptions);
1017
- const tokensBefore = estimateConversationTokens(messages);
1018
- const spinId = getId();
1019
- log("INFO", "compaction", `Running compaction`, {
1020
- messages: String(messages.length),
1021
- estimatedTokens: String(tokensBefore),
1022
- contextWindow: String(contextWindow),
1023
- });
1024
- // Show animated spinner
1025
- setLiveItems((prev) => [...prev, { kind: "compacting", id: spinId }]);
1026
- const ownedAbort = signal ? null : new AbortController();
1027
- const compactionSignal = signal ?? ownedAbort?.signal;
1028
- if (ownedAbort)
1029
- compactionAbortRef.current = ownedAbort;
1030
- try {
1031
- // Resolve fresh credentials for compaction too
1032
- let compactApiKey = activeApiKey;
1033
- let compactAccountId = activeAccountId;
1034
- let compactProjectId = activeProjectId;
1035
- let compactBaseUrl = activeBaseUrl;
1036
- if (props.authStorage) {
1037
- const creds = await props.authStorage.resolveCredentials(currentProvider);
1038
- compactApiKey = creds.accessToken;
1039
- compactAccountId = creds.accountId;
1040
- compactProjectId = creds.projectId;
1041
- compactBaseUrl = creds.baseUrl ?? compactBaseUrl;
1042
- }
1043
- const result = await compact(messages, {
1044
- provider: currentProvider,
1045
- model: currentModel,
1046
- apiKey: compactApiKey,
1047
- accountId: compactAccountId,
1048
- projectId: compactProjectId,
1049
- baseUrl: compactBaseUrl,
1050
- contextWindow,
1051
- signal: compactionSignal,
1052
- approvedPlanPath: approvedPlanPathRef.current,
1053
- });
1054
- if (result.result.compacted) {
1055
- // Replace spinner with completed notice
1056
- setLiveItems((prev) => prev.map((item) => item.id === spinId
1057
- ? {
1058
- kind: "compacted",
1059
- originalCount: result.result.originalCount,
1060
- newCount: result.result.newCount,
1061
- tokensBefore: result.result.tokensBeforeEstimate,
1062
- tokensAfter: result.result.tokensAfterEstimate,
1063
- id: spinId,
1064
- }
1065
- : item));
1066
- }
1067
- else {
1068
- // Nothing was actually compacted — remove spinner silently
1069
- log("INFO", "compaction", `Compaction skipped: ${result.result.reason ?? "unknown"}`);
1070
- setLiveItems((prev) => prev.filter((item) => item.id !== spinId));
1071
- }
1072
- return result.messages;
1073
- }
1074
- catch (err) {
1075
- const msg = err instanceof Error ? err.message : String(err);
1076
- const isAbort = compactionSignal?.aborted || msg.includes("aborted") || msg.includes("abort");
1077
- log(isAbort ? "WARN" : "ERROR", "compaction", isAbort ? "Compaction aborted" : `Compaction failed: ${msg}`);
1078
- setLiveItems((prev) => isAbort
1079
- ? prev.filter((item) => item.id !== spinId)
1080
- : prev.map((item) => item.id === spinId ? toErrorItem(err, spinId, "Compaction failed") : item));
1081
- return messages; // Return unchanged on failure/abort
1082
- }
1083
- finally {
1084
- if (ownedAbort && compactionAbortRef.current === ownedAbort)
1085
- compactionAbortRef.current = null;
1086
- }
1087
- }, [
725
+ const { compactionAbortRef, compactConversation, transformContext } = useContextCompaction({
1088
726
  currentModel,
1089
727
  currentProvider,
728
+ maxTokens: props.maxTokens,
729
+ authStorage: props.authStorage,
730
+ contextWindowOptions,
1090
731
  activeApiKey,
1091
732
  activeAccountId,
1092
733
  activeProjectId,
1093
734
  activeBaseUrl,
1094
- contextWindowOptions,
1095
- props.authStorage,
1096
- ]);
1097
- const getRepoMapSignalCount = useCallback(() => {
1098
- return ((props.repoMapChangedFilesRef?.current.size ?? 0) +
1099
- (props.repoMapReadFilesRef?.current.size ?? 0));
1100
- }, [props.repoMapChangedFilesRef, props.repoMapReadFilesRef]);
1101
- const getRepoMapBudget = useCallback(() => {
1102
- const userTurns = messagesRef.current.filter((message) => message.role === "user").length;
1103
- const readCount = props.repoMapReadFilesRef?.current.size ?? 0;
1104
- if (userTurns <= 1 && readCount === 0)
1105
- return FIRST_TURN_REPO_MAP_MAX_CHARS;
1106
- if (readCount > 0)
1107
- return FOCUSED_REPO_MAP_MAX_CHARS;
1108
- return FOCUSED_REPO_MAP_MAX_CHARS + 1000;
1109
- }, [props.repoMapReadFilesRef]);
1110
- const refreshRepoMap = useCallback(async (latestUserPrompt) => {
1111
- const rendered = await buildRepoMap({
1112
- cwd: cwdRef.current,
1113
- maxChars: getRepoMapBudget(),
1114
- changedFiles: [...(props.repoMapChangedFilesRef?.current ?? new Set())],
1115
- readFiles: [...(props.repoMapReadFilesRef?.current ?? new Set())],
1116
- focusTerms: latestUserPrompt ? [latestUserPrompt] : [],
1117
- cache: repoMapCacheRef.current,
1118
- });
1119
- repoMapMarkdownRef.current = rendered.markdown;
1120
- repoMapSnapshotRef.current = rendered.snapshot;
1121
- repoMapChangedCountRef.current = getRepoMapSignalCount();
1122
- repoMapDirtyRef.current = false;
1123
- return rendered.markdown;
1124
- }, [
1125
- getRepoMapBudget,
1126
- getRepoMapSignalCount,
1127
- props.repoMapChangedFilesRef,
1128
- props.repoMapReadFilesRef,
1129
- ]);
1130
- const stripRepoMapMessages = useCallback((messages) => {
1131
- return stripRepoMapContextMessages(messages);
1132
- }, []);
1133
- const injectRepoMapContext = useCallback(async (messages) => {
1134
- if (!repoMapInjectionEnabledRef.current)
1135
- return stripRepoMapMessages(messages);
1136
- const stripped = stripRepoMapMessages(messages);
1137
- const latestUserPrompt = getLatestUserText(stripped);
1138
- const signalCount = getRepoMapSignalCount();
1139
- if (signalCount !== repoMapChangedCountRef.current)
1140
- repoMapDirtyRef.current = true;
1141
- if (repoMapDirtyRef.current || !repoMapMarkdownRef.current) {
1142
- await refreshRepoMap(latestUserPrompt);
1143
- }
1144
- if (!repoMapMarkdownRef.current)
1145
- return stripped;
1146
- return injectRepoMapContextMessages(stripped, repoMapMarkdownRef.current);
1147
- }, [props.repoMapChangedFilesRef, props.repoMapReadFilesRef, refreshRepoMap, stripRepoMapMessages]);
1148
- /**
1149
- * transformContext callback for the agent loop.
1150
- * Called before each LLM call and on context overflow.
1151
- * Compacts persistent chat only, then injects the dynamic repo map transiently.
1152
- */
1153
- const transformContext = useCallback(async (messages, options) => {
1154
- const stripped = stripRepoMapMessages(messages);
1155
- const settings = settingsRef.current;
1156
- const autoCompact = settings?.get("autoCompact") ?? true;
1157
- const threshold = settings?.get("compactThreshold") ?? 0.8;
1158
- // Force-compact on context overflow regardless of settings
1159
- if (options?.force) {
1160
- const result = await compactConversation(stripped);
1161
- if (result !== stripped) {
1162
- messagesRef.current = result;
1163
- await persistCompactedSession(result);
1164
- }
1165
- lastCompactionTimeRef.current = Date.now();
1166
- return injectRepoMapContext(result);
1167
- }
1168
- if (!autoCompact)
1169
- return injectRepoMapContext(stripped);
1170
- // Time-based cooldown: skip if compaction ran within the last 30 seconds
1171
- if (Date.now() - lastCompactionTimeRef.current < 30_000) {
1172
- log("INFO", "compaction", `Skipping compaction — cooldown active`);
1173
- return injectRepoMapContext(stripped);
1174
- }
1175
- const contextWindow = getContextWindow(currentModel, contextWindowOptions);
1176
- const reserveTokens = getCompactionReserveTokens(props.maxTokens);
1177
- const tokensFresh = lastActualTokensTimestampRef.current > lastCompactionTimeRef.current;
1178
- const actualTokens = lastActualTokensRef.current > 0 && tokensFresh ? lastActualTokensRef.current : undefined;
1179
- if (shouldCompact(stripped, contextWindow, threshold, actualTokens, reserveTokens)) {
1180
- const result = await compactConversation(stripped);
1181
- if (result !== stripped) {
1182
- messagesRef.current = result;
1183
- await persistCompactedSession(result);
1184
- }
1185
- lastCompactionTimeRef.current = Date.now();
1186
- return injectRepoMapContext(result);
1187
- }
1188
- return injectRepoMapContext(stripped);
1189
- }, [
1190
- currentModel,
1191
- compactConversation,
1192
- contextWindowOptions,
1193
- injectRepoMapContext,
735
+ setLiveItems,
736
+ getId,
737
+ approvedPlanPathRef,
738
+ settingsRef,
739
+ messagesRef,
740
+ lastActualTokensRef,
741
+ lastActualTokensTimestampRef,
1194
742
  persistCompactedSession,
1195
- stripRepoMapMessages,
1196
- ]);
743
+ });
1197
744
  // ── Background task bar state (external store) ──────────
1198
745
  const { bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, } = useTaskBarStore();
1199
746
  useTaskBarPolling(props.processManager);
@@ -1225,16 +772,56 @@ export function App(props) {
1225
772
  tools: currentTools,
1226
773
  webSearch: props.webSearch,
1227
774
  maxTokens: props.maxTokens,
1228
- thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
775
+ thinking: thinkingLevel,
1229
776
  apiKey: activeApiKey,
1230
777
  baseUrl: activeBaseUrl,
1231
778
  accountId: activeAccountId,
1232
779
  projectId: activeProjectId,
1233
780
  resolveCredentials,
1234
781
  transformContext,
782
+ getIdealReviewMessage: (stats) => {
783
+ if (!idealReviewEnabledRef.current)
784
+ return null;
785
+ const decision = evaluateIdealReview(stats);
786
+ if (!decision.shouldReview)
787
+ return null;
788
+ log("INFO", "ideal", "Injecting ideal review before final response", {
789
+ score: String(decision.score),
790
+ reasons: decision.reasons.join(", "),
791
+ });
792
+ setLiveItems((prev) => [
793
+ ...prev,
794
+ { kind: "ideal_hook", text: IDEAL_HOOK_NOTICE_TEXT, tone: "review", id: getId() },
795
+ ]);
796
+ return buildIdealReviewMessage(decision.reasons);
797
+ },
798
+ getLoopBreakMessage: (stats) => {
799
+ if (!idealReviewEnabledRef.current)
800
+ return null;
801
+ const decision = evaluateLoopBreak(stats);
802
+ if (!decision.shouldBreak)
803
+ return null;
804
+ log("INFO", "loop-break", "Injecting loop-break nudge", {
805
+ reasons: decision.reasons.join(", "),
806
+ });
807
+ setLiveItems((prev) => [
808
+ ...prev,
809
+ { kind: "ideal_hook", text: LOOP_BREAK_NOTICE_TEXT, tone: "warning", id: getId() },
810
+ ]);
811
+ return buildLoopBreakMessage(decision.reasons);
812
+ },
813
+ getRegroundingMessage: (originalRequest) => {
814
+ if (!idealReviewEnabledRef.current)
815
+ return null;
816
+ log("INFO", "reground", "Injecting re-grounding after compaction", {});
817
+ setLiveItems((prev) => [
818
+ ...prev,
819
+ { kind: "ideal_hook", text: REGROUNDING_NOTICE_TEXT, tone: "info", id: getId() },
820
+ ]);
821
+ return buildRegroundingMessage(originalRequest);
822
+ },
1235
823
  }, {
1236
824
  onComplete: useCallback(() => {
1237
- messagesRef.current = stripRepoMapMessages(messagesRef.current);
1238
825
  persistNewMessages();
1239
826
  // Auto-clear plan progress and approved plan when all steps are completed
1240
827
  const steps = planStepsRef.current;
@@ -1287,8 +874,6 @@ export function App(props) {
1287
874
  }
1288
875
  }, [
1289
876
  persistNewMessages,
1290
- stripRepoMapMessages,
1291
- planMode,
1292
877
  props.cwd,
1293
878
  props.skills,
1294
879
  currentProvider,
@@ -1298,6 +883,8 @@ export function App(props) {
1298
883
  resolveCredentials,
1299
884
  ]),
1300
885
  onTurnText: useCallback((text, thinking, thinkingMs) => {
886
+ const hadStreamedAssistantFlush = streamedAssistantFlushRef.current.flushedChars > 0;
887
+ const unflushedAssistantText = text.slice(streamedAssistantFlushRef.current.flushedChars);
1301
888
  // Track [DONE:n] markers for plan step progress
1302
889
  if (planStepsRef.current.length > 0) {
1303
890
  const completed = findCompletedMarkers(text);
@@ -1312,15 +899,9 @@ export function App(props) {
1312
899
  followUpNudgesRef.current = { step: 0, count: 0 };
1313
900
  }
1314
901
  }
1315
- // Flush all completed items from the previous turn to Static history.
1316
- // This keeps liveItems bounded per-turn, preventing Ink's live area from
1317
- // growing unbounded, which makes Ink's live-area re-renders expensive.
1318
- //
1319
- // Items are queued in pendingFlushRef (not sent to setHistory directly)
1320
- // so the Static write happens in a SEPARATE render cycle from the
1321
- // live-area change — avoiding both Ink cursor-math clipping and the
1322
- // brief duplicate that occurred when setHistory was nested inside the
1323
- // setLiveItems updater.
902
+ // Flush completed rows from the previous turn to finalized terminal
903
+ // history. Ink keeps only the active turn, preventing live-area growth
904
+ // and avoiding Static/log-update replay during resize/remount churn.
1324
905
  setLiveItems((prev) => {
1325
906
  const flushed = flushOnTurnText(prev);
1326
907
  if (flushed.length > 0) {
@@ -1329,10 +910,7 @@ export function App(props) {
1329
910
  // Split text on [DONE:N] markers so each marker renders inline as
1330
911
  // a styled "✓ Step N: <description>" item at the position the
1331
912
  // agent emitted it, instead of vanishing into stripped whitespace.
1332
- // Falls back to a single assistant item containing the
1333
- // marker-stripped text when there are no markers (keeps the
1334
- // common case zero-cost).
1335
- const segments = segmentDisplayText(text, planStepsRef.current);
913
+ const segments = segmentDisplayText(unflushedAssistantText, planStepsRef.current);
1336
914
  const items = [];
1337
915
  let thinkingAttached = false;
1338
916
  for (const seg of segments) {
@@ -1345,7 +923,7 @@ export function App(props) {
1345
923
  // contains multiple text chunks split by markers.
1346
924
  thinking: thinkingAttached ? undefined : thinking,
1347
925
  thinkingMs: thinkingAttached ? undefined : thinkingMs,
1348
- planMode: planModeLocalRef.current,
926
+ continuation: hadStreamedAssistantFlush,
1349
927
  id: getId(),
1350
928
  });
1351
929
  thinkingAttached = true;
@@ -1360,36 +938,54 @@ export function App(props) {
1360
938
  }
1361
939
  }
1362
940
  // No segments at all (text was empty/whitespace, no markers).
1363
- // Still emit an assistant item so a thinking block renders if
1364
- // there was thinking content for this turn.
941
+ // Still persist an assistant item so a thinking block renders in
942
+ // terminal history if there was thinking content for this turn.
1365
943
  if (items.length === 0) {
1366
944
  items.push({
1367
945
  kind: "assistant",
1368
946
  text: "",
1369
947
  thinking,
1370
948
  thinkingMs,
1371
- planMode: planModeLocalRef.current,
1372
949
  id: getId(),
1373
950
  });
1374
951
  }
1375
- return items;
952
+ const assistantItems = prev.filter((item) => item.kind === "assistant");
953
+ const newAssistantText = normalizeAssistantText(unflushedAssistantText);
954
+ const duplicatePinnedText = newAssistantText.length > 0 &&
955
+ [...assistantItems, ...pendingHistoryFlushRef.current, ...historyRef.current].some((item) => isSameAssistantText(item, newAssistantText));
956
+ const nextItems = duplicatePinnedText
957
+ ? items.filter((item) => !isSameAssistantText(item, newAssistantText))
958
+ : items;
959
+ const flushablePrev = prev.filter((item) => item.kind !== "assistant");
960
+ if (flushablePrev.length > 0)
961
+ queueFlush(flushablePrev);
962
+ streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
963
+ return [...assistantItems, ...nextItems];
1376
964
  });
1377
- }, []),
1378
- onToolStart: useCallback((toolCallId, name, args) => {
965
+ }, [queueFlush]),
966
+ onToolStart: useCallback((toolCallId, name, args, stream) => {
1379
967
  log("INFO", "tool", `Tool call started: ${name}`, { id: toolCallId });
1380
968
  const startedAt = Date.now();
1381
969
  const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
1382
- // Flush completed items (assistant text, finished tools) to Static
1383
- // before adding tool UI. Keeping both in the live area makes it tall
1384
- // and causes Ink's cursor math to clip the top.
1385
- setLiveItems((prev) => {
1386
- const { flushed, remaining } = partitionCompleted(prev);
970
+ // Feed the pinned LiveToolPanel. Keep a small tail (panel shows the
971
+ // last few rows) so memory stays bounded across long sessions.
972
+ setLiveToolFeed((prev) => [...prev, { id: toolCallId, name, args, status: "running" }].slice(-(LIVE_TOOL_PANEL_ROWS * 2)));
973
+ const appendToolStart = (prev) => {
974
+ const visible = pinStreamingTextBeforeToolBoundary({
975
+ items: prev,
976
+ visibleStreamingText: stream.text,
977
+ thinking: stream.thinking,
978
+ thinkingMs: stream.thinkingMs,
979
+ makeId: getId,
980
+ });
981
+ const { flushed, remaining } = partitionCompleted(visible);
1387
982
  if (flushed.length > 0) {
1388
983
  queueFlush(flushed);
1389
984
  }
1390
985
  return remaining;
1391
- });
986
+ };
1392
987
  if (name === "subagent") {
988
+ setLiveItems(appendToolStart);
1393
989
  // Create or update the sub-agent group item
1394
990
  const newAgent = {
1395
991
  toolCallId,
@@ -1414,25 +1010,32 @@ export function App(props) {
1414
1010
  });
1415
1011
  }
1416
1012
  else if (AGGREGATABLE_TOOLS.has(name)) {
1417
- // Group concurrent read-only tools into a single compact item
1418
1013
  setLiveItems((prev) => {
1419
- // Find an active tool group (has at least one running tool)
1420
- const groupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
1421
- item.tools.some((t) => t.status === "running"));
1422
- if (groupIdx !== -1) {
1423
- const group = prev[groupIdx];
1424
- const next = [...prev];
1425
- next[groupIdx] = {
1426
- ...group,
1427
- tools: [
1428
- ...group.tools,
1429
- { toolCallId, name, args, status: "running", animateUntil },
1430
- ],
1431
- };
1432
- return next;
1014
+ const reusableGroupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
1015
+ item.tools.every((tool) => tool.name === name && !tool.isError));
1016
+ const prior = reusableGroupIdx === -1 ? [] : prev.slice(0, reusableGroupIdx);
1017
+ if (reusableGroupIdx !== -1 && prior.every((item) => !isActiveItem(item))) {
1018
+ const flushablePrior = prior.filter((item) => item.kind !== "assistant");
1019
+ if (flushablePrior.length > 0)
1020
+ queueFlush(flushablePrior);
1021
+ const pinnedPrior = prior.filter((item) => item.kind === "assistant");
1022
+ const candidates = prev.slice(reusableGroupIdx);
1023
+ const group = candidates[0];
1024
+ return [
1025
+ ...pinnedPrior,
1026
+ {
1027
+ ...group,
1028
+ tools: [
1029
+ ...group.tools,
1030
+ { toolCallId, name, args, status: "running", animateUntil },
1031
+ ],
1032
+ },
1033
+ ...candidates.slice(1),
1034
+ ];
1433
1035
  }
1036
+ const remaining = appendToolStart(prev);
1434
1037
  return [
1435
- ...prev,
1038
+ ...remaining,
1436
1039
  {
1437
1040
  kind: "tool_group",
1438
1041
  tools: [{ toolCallId, name, args, status: "running", animateUntil }],
@@ -1443,11 +1046,11 @@ export function App(props) {
1443
1046
  }
1444
1047
  else {
1445
1048
  setLiveItems((prev) => [
1446
- ...prev,
1049
+ ...appendToolStart(prev),
1447
1050
  { kind: "tool_start", toolCallId, name, args, id: getId(), startedAt, animateUntil },
1448
1051
  ]);
1449
1052
  }
1450
- }, []),
1053
+ }, [queueFlush]),
1451
1054
  onToolUpdate: useCallback((toolCallId, update) => {
1452
1055
  const u = update;
1453
1056
  // Bash progress streaming — append output to tool_start item
@@ -1489,6 +1092,17 @@ export function App(props) {
1489
1092
  });
1490
1093
  }, []),
1491
1094
  onToolEnd: useCallback((toolCallId, name, result, isError, durationMs, details) => {
1095
+ recordToolEnd(sessionStatsRef.current, name, isError, durationMs);
1096
+ setLiveToolFeed((prev) => prev.map((entry) => entry.id === toolCallId
1097
+ ? { ...entry, status: "done", isError, result, details }
1098
+ : entry));
1099
+ if (name === "edit" && !isError) {
1100
+ const diff = details?.diff ?? result;
1101
+ addLinesChanged(sessionStatsRef.current, {
1102
+ added: (diff.match(/^\+[^+]/gm) ?? []).length,
1103
+ removed: (diff.match(/^-[^-]/gm) ?? []).length,
1104
+ });
1105
+ }
1492
1106
  // Language-pack detection — gated on `write`/`bash` inside the
1493
1107
  // helper; cheap to call unconditionally. Fire-and-forget; the next
1494
1108
  // LLM turn picks up the swapped system prompt automatically.
@@ -1520,7 +1134,7 @@ export function App(props) {
1520
1134
  };
1521
1135
  const next = [...prev];
1522
1136
  next[groupIdx] = { ...group, agents: updatedAgents };
1523
- // Flush completed items to Static to keep the live area small
1137
+ // Flush completed items to finalized history to keep the live area small
1524
1138
  const { flushed, remaining } = partitionCompleted(next);
1525
1139
  if (flushed.length > 0) {
1526
1140
  queueFlush(flushed);
@@ -1530,6 +1144,19 @@ export function App(props) {
1530
1144
  }
1531
1145
  else {
1532
1146
  setLiveItems((prev) => {
1147
+ if (name === "enter_plan") {
1148
+ const updated = prev
1149
+ .filter((item) => !(item.kind === "tool_start" && item.toolCallId === toolCallId))
1150
+ .map((item) => item.kind === "plan_transition" && item.active
1151
+ ? { ...item, active: false }
1152
+ : item);
1153
+ const { flushed, remaining } = partitionCompleted(updated);
1154
+ if (flushed.length > 0) {
1155
+ queueFlush(flushed);
1156
+ return remaining;
1157
+ }
1158
+ return updated;
1159
+ }
1533
1160
  // Check if this tool is in a tool_group
1534
1161
  const groupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
1535
1162
  item.tools.some((t) => t.toolCallId === toolCallId));
@@ -1557,6 +1184,7 @@ export function App(props) {
1557
1184
  isError,
1558
1185
  durationMs,
1559
1186
  details,
1187
+ imagePreviews: extractToolImagePreviews(details),
1560
1188
  id: startItem.id,
1561
1189
  };
1562
1190
  updated = [...prev];
@@ -1574,12 +1202,13 @@ export function App(props) {
1574
1202
  isError,
1575
1203
  durationMs,
1576
1204
  details,
1205
+ imagePreviews: extractToolImagePreviews(details),
1577
1206
  id: getId(),
1578
1207
  },
1579
1208
  ];
1580
1209
  }
1581
1210
  }
1582
- // Flush completed items to Static to keep the live area small
1211
+ // Flush completed items to finalized history to keep the live area small
1583
1212
  const { flushed, remaining } = partitionCompleted(updated);
1584
1213
  if (flushed.length > 0) {
1585
1214
  queueFlush(flushed);
@@ -1595,14 +1224,22 @@ export function App(props) {
1595
1224
  });
1596
1225
  }
1597
1226
  }, []),
1598
- onServerToolCall: useCallback((id, name, input) => {
1227
+ onServerToolCall: useCallback((id, name, input, stream) => {
1228
+ recordServerToolCall(sessionStatsRef.current);
1599
1229
  log("INFO", "server_tool", `Server tool call: ${name}`, { id });
1600
1230
  const startedAt = Date.now();
1601
1231
  const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
1602
- // Flush completed items (including assistant text) to Static before
1603
- // adding server tool UI — same rationale as onToolStart.
1232
+ // Flush completed items (including assistant text) before adding server
1233
+ // tool UI — same rationale as onToolStart.
1604
1234
  setLiveItems((prev) => {
1605
- const { flushed, remaining } = partitionCompleted(prev);
1235
+ const visible = pinStreamingTextBeforeToolBoundary({
1236
+ items: prev,
1237
+ visibleStreamingText: stream.text,
1238
+ thinking: stream.thinking,
1239
+ thinkingMs: stream.thinkingMs,
1240
+ makeId: getId,
1241
+ });
1242
+ const { flushed, remaining } = partitionCompleted(visible);
1606
1243
  if (flushed.length > 0) {
1607
1244
  queueFlush(flushed);
1608
1245
  }
@@ -1619,7 +1256,7 @@ export function App(props) {
1619
1256
  },
1620
1257
  ];
1621
1258
  });
1622
- }, []),
1259
+ }, [queueFlush]),
1623
1260
  onServerToolResult: useCallback((toolUseId, resultType, data) => {
1624
1261
  log("INFO", "server_tool", `Server tool result`, { toolUseId, resultType });
1625
1262
  setLiveItems((prev) => {
@@ -1653,15 +1290,16 @@ export function App(props) {
1653
1290
  },
1654
1291
  ];
1655
1292
  }
1656
- // Flush completed items to Static
1293
+ // Flush completed items to finalized history
1657
1294
  const { flushed, remaining } = partitionCompleted(updated);
1658
1295
  if (flushed.length > 0) {
1659
1296
  queueFlush(flushed);
1660
1297
  }
1661
1298
  return remaining;
1662
1299
  });
1663
- }, []),
1300
+ }, [queueFlush]),
1664
1301
  onTurnEnd: useCallback((turn, stopReason, usage) => {
1302
+ recordTurnEnd(sessionStatsRef.current, usage);
1665
1303
  log("INFO", "turn", `Turn ${turn} ended`, {
1666
1304
  stopReason,
1667
1305
  inputTokens: String(usage.inputTokens),
@@ -1676,8 +1314,8 @@ export function App(props) {
1676
1314
  lastActualTokensRef.current =
1677
1315
  currentProvider === "anthropic" ? inputContext : inputContext + usage.outputTokens;
1678
1316
  lastActualTokensTimestampRef.current = Date.now();
1679
- // For tool-only turns (no text), flush completed items to Static so
1680
- // liveItems doesn't grow unbounded across consecutive tool-only turns.
1317
+ // For tool-only turns (no text), flush completed items to finalized
1318
+ // history so liveItems doesn't grow unbounded across consecutive turns.
1681
1319
  setLiveItems((prev) => {
1682
1320
  const { flushed, remaining } = flushOnTurnEnd(prev, stopReason);
1683
1321
  if (flushed.length > 0) {
@@ -1685,76 +1323,52 @@ export function App(props) {
1685
1323
  }
1686
1324
  return remaining;
1687
1325
  });
1688
- }, []),
1689
- onDone: useCallback((durationMs, toolsUsed, errored) => {
1326
+ }, [queueFlush]),
1327
+ onDone: useCallback((durationMs, toolsUsed) => {
1690
1328
  log("INFO", "agent", `Agent done`, {
1691
1329
  duration: `${durationMs}ms`,
1692
1330
  toolsUsed: toolsUsed.join(",") || "none",
1693
1331
  });
1694
- // Don't show "done" status when plan overlay is about to open —
1695
- // the agent loop finished but we're waiting for user plan review
1696
- if (planOverlayPendingRef.current)
1697
- return;
1698
- setDoneStatus({ durationMs, toolsUsed, verb: pickDurationVerb(toolsUsed) });
1699
- playNotificationSound();
1700
- // Two-phase flush to avoid Ink text clipping.
1701
- // Phase 1 (here): clear the live area so Ink commits a render with
1702
- // the smaller output and updates its internal line counter.
1703
- // Phase 2 (useEffect below): push items to Static history in a
1704
- // separate render cycle so the Static write never coincides with
1705
- // a live-area height change in the same frame.
1706
- setLiveItems((prev) => {
1707
- if (prev.length > 0)
1708
- queueFlush(prev);
1709
- return [];
1332
+ const doneDecision = getDoneFlushDecision({
1333
+ planOverlayPending: planOverlayPendingRef.current,
1710
1334
  });
1711
- // Run-all: auto-start next pending task after a short delay
1712
- // (allow the two-phase flush to complete first)
1335
+ // Don't show "done" status when the plan review pane is about to open —
1336
+ // the agent loop finished but we're waiting for user approval/review.
1337
+ // Still flush live transcript rows before the pane remounts; otherwise
1338
+ // setup output remains in ephemeral liveItems and appears to vanish.
1339
+ if (doneDecision.showDoneStatus) {
1340
+ setDoneStatus({ durationMs, toolsUsed, verb: pickDurationVerb(toolsUsed) });
1341
+ playNotificationSound();
1342
+ }
1343
+ // Keep the final assistant response mounted in the live frame after a
1344
+ // normal chat turn finishes. Moving a large final response to terminal
1345
+ // history at this moment writes many scrollback rows while the footer is
1346
+ // still mounted, which visibly pushes the input/footer upward. The final
1347
+ // response is flushed on the next submit before the new prompt is shown.
1348
+ // Non-chat overlay transitions still flush so setup/plan output
1349
+ // does not vanish during remounts.
1350
+ if (doneDecision.flushLiveItems && !doneDecision.showDoneStatus) {
1351
+ setLiveItems((prev) => {
1352
+ if (prev.length > 0)
1353
+ queueFlush(prev);
1354
+ return prev;
1355
+ });
1356
+ }
1357
+ // Run-all: auto-start next pending task after a short delay.
1713
1358
  if (runAllTasksRef.current) {
1714
1359
  setTimeout(() => {
1715
1360
  const cwd = cwdRef.current;
1716
- // Clean turn: mark the in-flight task done so the row flips to ✓.
1717
- // Error path: leave it at `in-progress` so the failed task is
1718
- // visibly distinct from completed ones. The chain still advances
1719
- // either way — matching how errors were handled before this fix.
1720
- const finishedId = currentTaskIdRef.current;
1721
- if (finishedId) {
1722
- if (!errored)
1723
- markTaskDone(cwd, finishedId);
1724
- currentTaskIdRef.current = null;
1725
- if (props.sessionStore)
1726
- props.sessionStore.currentTaskId = null;
1727
- }
1728
1361
  const next = getNextPendingTask(cwd);
1729
1362
  if (next) {
1730
1363
  markTaskInProgress(cwd, next.id);
1731
- // startTaskRef.current() re-sets taskRunningRef = true before
1732
- // the next agent run begins, so the brief clear-then-set is
1733
- // safe — the agent isn't running in between.
1734
1364
  startTaskRef.current(next.title, next.prompt, next.id);
1735
1365
  }
1736
1366
  else {
1737
1367
  setRunAllTasks(false);
1738
- if (props.taskRunningRef)
1739
- props.taskRunningRef.current = false;
1740
1368
  log("INFO", "tasks", "Run-all complete — no more pending tasks");
1741
1369
  }
1742
1370
  }, 500);
1743
1371
  }
1744
- else if (currentTaskIdRef.current) {
1745
- // Single manual task done — clear the task-running flag so a
1746
- // subsequent free-form prompt can use plan mode again.
1747
- if (props.taskRunningRef)
1748
- props.taskRunningRef.current = false;
1749
- currentTaskIdRef.current = null;
1750
- if (props.sessionStore)
1751
- props.sessionStore.currentTaskId = null;
1752
- }
1753
- // Goal loop: after the orchestrator handles a worker/verifier event,
1754
- // continue the same Goal automatically until it reaches a terminal state.
1755
- for (const runId of [...runningGoalIdsRef.current]) {
1756
- setTimeout(() => continueGoalRun(runId), 500);
1757
- }
1758
1372
  // Pixel fix: observe branch + commits, patch status, optionally pick
1759
1373
  // up the next open error if run-all is active.
1760
1374
  const pendingFix = currentPixelFixRef.current;
@@ -1796,16 +1410,6 @@ export function App(props) {
1796
1410
  setRunAllTasks(false);
1797
1411
  setRunAllPixel(false);
1798
1412
  currentPixelFixRef.current = null;
1799
- // Clear the task-running flag so a subsequent free-form prompt can
1800
- // re-enable plan mode. Aborted runs never auto-advance.
1801
- if (props.taskRunningRef)
1802
- props.taskRunningRef.current = false;
1803
- // Clear the run-all task id so a subsequent run-all does not
1804
- // mistakenly mark this aborted task as done. The on-disk row stays
1805
- // at `in-progress`, which is the correct truth for an aborted task.
1806
- currentTaskIdRef.current = null;
1807
- if (props.sessionStore)
1808
- props.sessionStore.currentTaskId = null;
1809
1413
  setDoneStatus(null);
1810
1414
  setLiveItems((prev) => {
1811
1415
  const next = prev.map((item) => {
@@ -1851,42 +1455,16 @@ export function App(props) {
1851
1455
  }, []),
1852
1456
  onQueuedStart: useCallback((content) => {
1853
1457
  // When a queued message starts processing, show it as a UserItem
1854
- // and flush prior items to history. Synthetic system events are hidden
1855
- // from the transcript but still routed through the main agent context.
1458
+ // and flush prior items to history.
1856
1459
  const displayText = typeof content === "string"
1857
1460
  ? content
1858
1461
  : content
1859
1462
  .filter((c) => c.type === "text")
1860
1463
  .map((c) => c.text)
1861
1464
  .join("\n");
1862
- if (isGoalSyntheticEvent(displayText)) {
1863
- const eventInfo = parseGoalSyntheticEvent(displayText);
1864
- setLiveItems((prev) => {
1865
- if (prev.length > 0)
1866
- queueFlush(prev);
1867
- return [];
1868
- });
1869
- setDoneStatus(null);
1870
- appendGoalProgress({
1871
- kind: "goal_progress",
1872
- phase: "orchestrator_reviewing",
1873
- title: "Orchestrator reviewing Goal update",
1874
- detail: eventInfo?.kind === "worker"
1875
- ? `Worker ${eventInfo.worker ?? "finished"} reported back${eventInfo.task ? ` on ${eventInfo.task}` : ""}. Inspecting Goal state.`
1876
- : `Verifier reported ${eventInfo?.status ?? "status"}. Inspecting evidence and next action.`,
1877
- workerId: eventInfo?.worker,
1878
- status: eventInfo?.status,
1879
- });
1880
- return;
1881
- }
1882
1465
  const imageCount = typeof content === "string"
1883
1466
  ? undefined
1884
1467
  : content.filter((c) => c.type === "image").length || undefined;
1885
- setLiveItems((prev) => {
1886
- if (prev.length > 0)
1887
- queueFlush(prev);
1888
- return [];
1889
- });
1890
1468
  const userItem = {
1891
1469
  kind: "user",
1892
1470
  text: displayText,
@@ -1895,8 +1473,8 @@ export function App(props) {
1895
1473
  };
1896
1474
  setLastUserMessage(displayText);
1897
1475
  setDoneStatus(null);
1898
- setLiveItems([userItem]);
1899
- }, []),
1476
+ finalizeSubmittedUserItem(userItem);
1477
+ }, [finalizeSubmittedUserItem]),
1900
1478
  // Inject a "continue with the next step" follow-up when the agent
1901
1479
  // would otherwise stop mid-plan. The prompt-only instruction wasn't
1902
1480
  // enough — some models (notably Opus) treat each [DONE:n] as a
@@ -1928,9 +1506,280 @@ export function App(props) {
1928
1506
  ];
1929
1507
  }, []),
1930
1508
  });
1931
- // First-time-per-project auto-run of /setup. Bound after `agentLoop` is in
1932
- // scope so the ref closure can dispatch to it. Called from the initial
1933
- // language-detection path when `isFirstTimeSetup(cwd)` is true. Pushes a
1509
+ const continueGoalRun = useCallback((run) => {
1510
+ const cwd = cwdRef.current;
1511
+ const decision = decideGoalNextAction(run);
1512
+ const choiceKey = getGoalContinuationChoiceKey({ runId: run.id, decision });
1513
+ const now = Date.now();
1514
+ for (const [key, timestamp] of goalContinuationRecentChoicesRef.current.entries()) {
1515
+ if (now - timestamp > 5_000)
1516
+ goalContinuationRecentChoicesRef.current.delete(key);
1517
+ }
1518
+ if (goalContinuationFlightsRef.current.has(choiceKey))
1519
+ return;
1520
+ const lastChosenAt = goalContinuationRecentChoicesRef.current.get(choiceKey);
1521
+ if (lastChosenAt && now - lastChosenAt < 1_500)
1522
+ return;
1523
+ goalContinuationFlightsRef.current.add(choiceKey);
1524
+ goalContinuationRecentChoicesRef.current.set(choiceKey, now);
1525
+ const releaseChoice = () => {
1526
+ setTimeout(() => goalContinuationFlightsRef.current.delete(choiceKey), 250);
1527
+ };
1528
+ void (async () => {
1529
+ try {
1530
+ if (decision.kind === "create_task") {
1531
+ const nextTask = createGoalTask({ title: decision.title, prompt: decision.prompt });
1532
+ const updated = await upsertGoalRun(cwd, {
1533
+ ...run,
1534
+ status: "ready",
1535
+ tasks: [...run.tasks, nextTask],
1536
+ });
1537
+ await appendGoalDecision(cwd, run.id, decision);
1538
+ appendGoalProgress({
1539
+ kind: "goal_progress",
1540
+ phase: "orchestrator_working",
1541
+ title: `Goal ${goalNumberForRun(run.id)} task queued`,
1542
+ detail: decision.reason,
1543
+ status: updated.status,
1544
+ });
1545
+ continueGoalRun(updated);
1546
+ return;
1547
+ }
1548
+ if (decision.kind === "start_worker") {
1549
+ const updatedRun = await updateGoalTask(cwd, run.id, decision.task.id, {
1550
+ status: "running",
1551
+ attempts: decision.attempts,
1552
+ lastSummary: `Worker attempt ${decision.attempts} running.`,
1553
+ });
1554
+ const taskForPrompt = updatedRun?.tasks.find((task) => task.id === decision.task.id) ?? {
1555
+ ...decision.task,
1556
+ attempts: decision.attempts,
1557
+ };
1558
+ const worker = await startGoalWorker({
1559
+ cwd,
1560
+ goalRunId: run.id,
1561
+ goalTaskId: taskForPrompt.id,
1562
+ taskTitle: taskForPrompt.title,
1563
+ provider: currentProviderRef.current,
1564
+ model: currentModelRef.current,
1565
+ prompt: taskForPrompt.prompt,
1566
+ });
1567
+ upsertGoalStatusEntry({
1568
+ runId: run.id,
1569
+ label: run.title,
1570
+ phase: "worker",
1571
+ detail: `Worker: ${decision.task.title}`,
1572
+ workerId: worker.id,
1573
+ startedAt: Date.now(),
1574
+ });
1575
+ appendGoalProgress({
1576
+ kind: "goal_progress",
1577
+ phase: "worker_started",
1578
+ title: `Goal ${goalNumberForRun(run.id)} worker started`,
1579
+ detail: decision.task.title,
1580
+ workerId: worker.id,
1581
+ status: "running",
1582
+ });
1583
+ return;
1584
+ }
1585
+ if (decision.kind === "run_verifier") {
1586
+ if (activeVerifierRunIdsRef.current.has(run.id))
1587
+ return;
1588
+ activeVerifierRunIdsRef.current.add(run.id);
1589
+ upsertGoalStatusEntry({
1590
+ runId: run.id,
1591
+ label: run.title,
1592
+ phase: "verifier",
1593
+ detail: "Verifier running",
1594
+ startedAt: Date.now(),
1595
+ });
1596
+ appendGoalProgress({
1597
+ kind: "goal_progress",
1598
+ phase: "verifier_started",
1599
+ title: `Goal ${goalNumberForRun(run.id)} verifier started`,
1600
+ detail: decision.command,
1601
+ status: "running",
1602
+ });
1603
+ try {
1604
+ const completed = await runGoalVerifierCommand({
1605
+ cwd,
1606
+ runId: run.id,
1607
+ command: decision.command,
1608
+ });
1609
+ const verification = completed.verification;
1610
+ const verificationStatus = verification.status === "pass" ? "pass" : "fail";
1611
+ const latestBeforeVerifier = (await loadGoalRuns(cwd)).find((candidate) => candidate.id === run.id) ?? run;
1612
+ const latest = await upsertGoalRun(cwd, {
1613
+ ...latestBeforeVerifier,
1614
+ status: verificationStatus === "pass" ? "passed" : "ready",
1615
+ verifier: {
1616
+ ...(latestBeforeVerifier.verifier ?? { description: "Goal verifier" }),
1617
+ command: verification.command ?? decision.command,
1618
+ lastResult: verification,
1619
+ },
1620
+ });
1621
+ await appendGoalEvidence(cwd, run.id, {
1622
+ kind: "log",
1623
+ label: "Verifier result",
1624
+ content: formatGoalVerifierCompletionEvent(latest, verificationStatus, verification.command ?? decision.command, verification.exitCode ?? (verificationStatus === "pass" ? 0 : 1), verification.summary),
1625
+ ...(verification.outputPath ? { path: verification.outputPath } : {}),
1626
+ });
1627
+ appendGoalProgress({
1628
+ kind: "goal_progress",
1629
+ phase: "verifier_finished",
1630
+ title: `Goal ${goalNumberForRun(run.id)} verifier ${verificationStatus}`,
1631
+ detail: verification.summary,
1632
+ status: verificationStatus,
1633
+ });
1634
+ upsertGoalStatusEntry({
1635
+ runId: run.id,
1636
+ label: latest.title,
1637
+ phase: verificationStatus === "pass" ? "reviewing" : "failed",
1638
+ detail: verificationStatus === "pass" ? "Verifier passed" : "Verifier failed",
1639
+ startedAt: Date.now(),
1640
+ });
1641
+ continueGoalRun(latest);
1642
+ }
1643
+ catch (error) {
1644
+ appendGoalProgress({
1645
+ kind: "goal_progress",
1646
+ phase: "verifier_finished",
1647
+ title: `Goal ${goalNumberForRun(run.id)} verifier failed`,
1648
+ detail: error instanceof Error ? error.message : String(error),
1649
+ status: "failed",
1650
+ });
1651
+ }
1652
+ finally {
1653
+ activeVerifierRunIdsRef.current.delete(run.id);
1654
+ }
1655
+ return;
1656
+ }
1657
+ if (decision.kind === "complete") {
1658
+ const completion = canCompleteGoalRun(run);
1659
+ const updated = await upsertGoalRun(cwd, { ...run, status: "passed" });
1660
+ const summary = completion.ok
1661
+ ? "Verifier evidence proves the Goal success criteria."
1662
+ : goalCompletionReason(completion);
1663
+ await appendGoalDecision(cwd, run.id, {
1664
+ kind: "summary",
1665
+ content: summary,
1666
+ });
1667
+ appendGoalProgress({
1668
+ kind: "goal_progress",
1669
+ phase: "terminal",
1670
+ title: `Goal passed: ${updated.title}`,
1671
+ detail: summary,
1672
+ status: updated.status,
1673
+ });
1674
+ clearGoalStatusEntry(run.id);
1675
+ return;
1676
+ }
1677
+ if (decision.kind === "pause") {
1678
+ const updated = await upsertGoalRun(cwd, {
1679
+ ...run,
1680
+ status: "paused",
1681
+ blockers: [...run.blockers, decision.reason],
1682
+ });
1683
+ await appendGoalDecision(cwd, run.id, decision);
1684
+ upsertGoalStatusEntry({
1685
+ runId: updated.id,
1686
+ label: updated.title,
1687
+ phase: "failed",
1688
+ detail: decision.reason,
1689
+ startedAt: Date.now(),
1690
+ });
1691
+ appendGoalProgress({
1692
+ kind: "goal_progress",
1693
+ phase: "terminal",
1694
+ title: `Goal paused: ${updated.title}`,
1695
+ detail: decision.reason,
1696
+ summaryRows: buildGoalSummaryRows(updated),
1697
+ status: "paused",
1698
+ });
1699
+ return;
1700
+ }
1701
+ if (decision.kind === "terminal") {
1702
+ const updated = await upsertGoalRun(cwd, { ...run, status: decision.status });
1703
+ if (decision.status === "passed")
1704
+ clearGoalStatusEntry(run.id);
1705
+ appendGoalProgress({
1706
+ kind: "goal_progress",
1707
+ phase: "terminal",
1708
+ title: `Goal ${decision.status}: ${updated.title}`,
1709
+ detail: decision.reason,
1710
+ summaryRows: buildGoalSummaryRows(updated),
1711
+ status: decision.status,
1712
+ });
1713
+ return;
1714
+ }
1715
+ if (decision.kind === "blocked") {
1716
+ const updated = await upsertGoalRun(cwd, {
1717
+ ...run,
1718
+ status: "blocked",
1719
+ blockers: [...run.blockers, decision.reason],
1720
+ });
1721
+ upsertGoalStatusEntry({
1722
+ runId: updated.id,
1723
+ label: updated.title,
1724
+ phase: "failed",
1725
+ detail: decision.reason,
1726
+ startedAt: Date.now(),
1727
+ });
1728
+ appendGoalProgress({
1729
+ kind: "goal_progress",
1730
+ phase: "terminal",
1731
+ title: `Goal ${goalNumberForRun(run.id)} blocked`,
1732
+ detail: decision.reason,
1733
+ status: "blocked",
1734
+ });
1735
+ }
1736
+ }
1737
+ finally {
1738
+ releaseChoice();
1739
+ }
1740
+ })();
1741
+ }, [appendGoalProgress, clearGoalStatusEntry, goalNumberForRun, upsertGoalStatusEntry]);
1742
+ const handleGoalWorkerCompletion = useCallback((completion) => {
1743
+ const cwd = cwdRef.current;
1744
+ runningGoalIdsRef.current.delete(completion.worker.goalRunId);
1745
+ void (async () => {
1746
+ const run = (await loadGoalRuns(cwd)).find((candidate) => candidate.id === completion.worker.goalRunId);
1747
+ const taskTitle = run?.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
1748
+ completion.worker.goalTaskId;
1749
+ await appendGoalEvidence(cwd, completion.worker.goalRunId, {
1750
+ kind: "log",
1751
+ label: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
1752
+ content: run
1753
+ ? formatGoalWorkerCompletionEvent(run, taskTitle, completion)
1754
+ : completion.summary,
1755
+ path: completion.worker.logFile,
1756
+ });
1757
+ appendGoalProgress({
1758
+ kind: "goal_progress",
1759
+ phase: "worker_finished",
1760
+ status: completion.status === "done" ? "done" : "failed",
1761
+ title: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
1762
+ detail: summarizeGoalCompletion(completion.summary),
1763
+ workerId: completion.worker.id,
1764
+ });
1765
+ if (!run) {
1766
+ clearGoalStatusEntry(completion.worker.goalRunId);
1767
+ return;
1768
+ }
1769
+ upsertGoalStatusEntry({
1770
+ runId: run.id,
1771
+ label: run.title,
1772
+ phase: completion.status === "done" ? "orchestrating" : "failed",
1773
+ detail: completion.status === "done" ? "Worker finished" : "Worker failed",
1774
+ startedAt: Date.now(),
1775
+ });
1776
+ continueGoalRun(run);
1777
+ })();
1778
+ }, [appendGoalProgress, clearGoalStatusEntry, continueGoalRun, upsertGoalStatusEntry]);
1779
+ useEffect(() => subscribeGoalWorkerCompletions(handleGoalWorkerCompletion), [handleGoalWorkerCompletion]);
1780
+ // First-time-per-project auto-run of /setup. Bound after `agentLoop` is in
1781
+ // scope so the ref closure can dispatch to it. Called from the initial
1782
+ // language-detection path when `isFirstTimeSetup(cwd)` is true. Pushes a
1934
1783
  // notice item explaining what's happening, then runs the audit prompt.
1935
1784
  triggerAutoSetupRef.current = async () => {
1936
1785
  const setupCmd = getPromptCommand("setup");
@@ -1966,27 +1815,6 @@ export function App(props) {
1966
1815
  ]);
1967
1816
  }
1968
1817
  };
1969
- // Phase 2 of the two-phase flush: after onDone clears liveItems (phase 1)
1970
- // and Ink renders the smaller live area (updating its internal line
1971
- // counter), this effect pushes the stashed items into Static history.
1972
- // Because the Static write happens in a SEPARATE render cycle from the
1973
- // live-area shrink, Ink's log-update never needs to erase the old tall
1974
- // live area AND write Static content in the same frame — avoiding the
1975
- // cursor-math mismatch that caused text clipping.
1976
- useEffect(() => {
1977
- if (pendingFlushRef.current.length > 0) {
1978
- const items = pendingFlushRef.current;
1979
- pendingFlushRef.current = [];
1980
- setHistory((h) => {
1981
- const next = compactHistory([...h, ...trimFlushedItems(items)]);
1982
- if (sessionStore)
1983
- sessionStore.history = next;
1984
- return next;
1985
- });
1986
- if (sessionStore)
1987
- sessionStore.liveItems = liveItems;
1988
- }
1989
- }, [flushGeneration]);
1990
1818
  // Sync terminal title with agent loop state
1991
1819
  useEffect(() => {
1992
1820
  setTitleRunning(agentLoop.isRunning);
@@ -2014,10 +1842,28 @@ export function App(props) {
2014
1842
  return () => clearTimeout(timer);
2015
1843
  }
2016
1844
  }, [agentLoop.isRunning, sessionStore, props.resetUI]);
2017
- // Consume sessionStore.pendingAction once on mount. Set by resetUI options
2018
- // for paths that remount AND immediately drive the agent (plan accept,
2019
- // plan reject, startTask, pixel fix). The action survives the unmount
2020
- // because it lives in renderApp's closure (sessionStore), not React state.
1845
+ const showSessionSummaryAndExit = useCallback(() => {
1846
+ const summary = buildSessionSummary({
1847
+ stats: sessionStatsRef.current,
1848
+ provider: currentProvider,
1849
+ model: currentModel,
1850
+ cwd: displayedCwd,
1851
+ footer: sessionStatsRef.current.sessionId
1852
+ ? `To resume this session: ezcoder --resume ${sessionStatsRef.current.sessionId}`
1853
+ : undefined,
1854
+ });
1855
+ setDoneStatus(null);
1856
+ setExitPending(false);
1857
+ setOverlay(null);
1858
+ setLiveItems([]);
1859
+ setQuittingSummary(summary);
1860
+ writeStdout("\x1b[2J\x1b[3J\x1b[H");
1861
+ setTimeout(() => process.exit(0), 150);
1862
+ }, [currentModel, currentProvider, displayedCwd, writeStdout]);
1863
+ // Consume pending post-remount work once on mount. Set by resetUI options
1864
+ // for paths that remount AND immediately drive work (plan accept/reject,
1865
+ // pixel fix, plan accept/reject). The work survives the unmount because
1866
+ // it lives in renderApp's closure (sessionStore), not React state.
2021
1867
  useEffect(() => {
2022
1868
  if (pendingActionConsumedRef.current)
2023
1869
  return;
@@ -2025,8 +1871,10 @@ export function App(props) {
2025
1871
  if (!action)
2026
1872
  return;
2027
1873
  pendingActionConsumedRef.current = true;
2028
- if (sessionStore)
1874
+ if (sessionStore) {
2029
1875
  sessionStore.pendingAction = undefined;
1876
+ }
1877
+ setDoneStatus(null);
2030
1878
  if (action.planEvent) {
2031
1879
  const ev = action.planEvent;
2032
1880
  setLiveItems((prev) => [
@@ -2040,7 +1888,6 @@ export function App(props) {
2040
1888
  { kind: "info", text: action.infoText, id: getId() },
2041
1889
  ]);
2042
1890
  }
2043
- setDoneStatus(null);
2044
1891
  void agentLoop.run(action.prompt).catch((err) => {
2045
1892
  const errMsg = err instanceof Error ? err.message : String(err);
2046
1893
  log("ERROR", "error", errMsg);
@@ -2048,14 +1895,6 @@ export function App(props) {
2048
1895
  });
2049
1896
  // Intentional one-shot: run once on mount, never re-fire on re-render.
2050
1897
  }, []);
2051
- // Refresh eyes badge count when the agent settles (end of a turn) — a turn
2052
- // may have logged new rough/wish/blocked signals. Also covers the case where
2053
- // /eyes was run for the first time (manifest now exists).
2054
- useEffect(() => {
2055
- if (!agentLoop.isRunning) {
2056
- setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
2057
- }
2058
- }, [agentLoop.isRunning, props.cwd]);
2059
1898
  const handleSubmit = useCallback(async (input, inputImages = [], pasteInfo) => {
2060
1899
  const trimmed = input.trim();
2061
1900
  if (trimmed.startsWith("/")) {
@@ -2070,241 +1909,141 @@ export function App(props) {
2070
1909
  // has not grown.
2071
1910
  await applyLanguageDetectionRef.current("input");
2072
1911
  }
2073
- // Handle /model directly open inline selector
2074
- if (trimmed === "/model" || trimmed === "/m" || trimmed === "/models") {
2075
- setOverlay("model");
2076
- return;
2077
- }
2078
- // Handle /compact — compact conversation
2079
- if (trimmed === "/compact" || trimmed === "/c") {
2080
- const ac = new AbortController();
2081
- compactionAbortRef.current = ac;
2082
- const compacted = await compactConversation(messagesRef.current, ac.signal);
2083
- if (!ac.signal.aborted && compacted !== messagesRef.current) {
2084
- messagesRef.current = compacted;
2085
- await persistCompactedSession(compacted);
2086
- }
2087
- if (compactionAbortRef.current === ac)
2088
- compactionAbortRef.current = null;
2089
- return;
2090
- }
2091
- // Handle /quit — exit the agent
2092
- if (trimmed === "/quit" || trimmed === "/q" || trimmed === "/exit") {
2093
- process.exit(0);
2094
- }
2095
- // Handle /clear — tear down the entire Ink instance and rebuild fresh.
2096
- // Avoid direct ANSI terminal clears here; they can erase scrollback.
2097
- // Runtime state (model, provider, thinking) survives via renderApp's
2098
- // closure-held `runtimeState`, mirrored from React state via the
2099
- // useEffects above.
2100
- if (trimmed === "/clear") {
2101
- if (props.resetUI) {
2102
- void (async () => {
2103
- const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
2104
- props.resetUI?.({
2105
- wipeSession: true,
2106
- messages: [{ role: "system", content: newPrompt }],
2107
- });
2108
- })();
2109
- return;
2110
- }
2111
- // Fallback path (resetUI not wired — e.g. tests). Best-effort: clear
2112
- // React state in place without touching terminal scrollback.
2113
- pendingFlushRef.current = [];
2114
- setHistory([{ kind: "banner", id: "banner" }]);
2115
- setLiveItems([]);
2116
- setDoneStatus(null);
2117
- approvedPlanPathRef.current = undefined;
2118
- planStepsRef.current = [];
2119
- setPlanSteps([]);
2120
- void (async () => {
2121
- const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
2122
- messagesRef.current = [{ role: "system", content: newPrompt }];
2123
- persistedIndexRef.current = messagesRef.current.length;
2124
- })();
2125
- agentLoop.reset();
2126
- setSessionTitle(undefined);
2127
- sessionTitleGeneratedRef.current = false;
2128
- setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
2129
- return;
2130
- }
2131
- // Handle /theme — open theme selector overlay
2132
- if (trimmed === "/theme" || trimmed === "/t") {
2133
- setOverlay("theme");
2134
- return;
2135
- }
2136
- // Open the Eyes pane — read-only review of installed probes + open signals.
2137
- // Gated by the ezcoder-eyes manifest: in projects without /eyes set up,
2138
- // there's nothing useful to show.
2139
- if (trimmed === "/eyes-view" || trimmed === "/ev") {
2140
- if (!isEyesActive(props.cwd)) {
2141
- setLiveItems((prev) => [
2142
- ...prev,
2143
- {
2144
- kind: "info",
2145
- text: "Eyes not set up in this project. Run /setup-eyes to get started.",
2146
- id: getId(),
2147
- },
2148
- ]);
2149
- return;
1912
+ if (trimmed === "/ideal-on" || trimmed === "/ideal-off") {
1913
+ const next = trimmed === "/ideal-on";
1914
+ setIdealReviewEnabled(next);
1915
+ if (props.settingsFile) {
1916
+ const sm = new SettingsManager(props.settingsFile);
1917
+ await sm.load();
1918
+ await sm.set("idealReviewEnabled", next);
2150
1919
  }
2151
- setOverlay("eyes");
2152
- return;
2153
- }
2154
- // Handle /plan — toggle plan mode
2155
- if (trimmed === "/plan" || trimmed === "/plan on") {
2156
- setPlanMode(true);
2157
- setLiveItems((prev) => [
2158
- ...prev,
2159
- { kind: "plan_transition", text: "Plan Mode Activated", active: true, id: getId() },
2160
- ]);
2161
- return;
2162
- }
2163
- if (trimmed === "/plan off") {
2164
- setPlanMode(false);
2165
1920
  setLiveItems((prev) => [
2166
1921
  ...prev,
2167
1922
  {
2168
- kind: "plan_transition",
2169
- text: "Plan Mode Deactivated",
2170
- active: false,
1923
+ kind: "info",
1924
+ text: next
1925
+ ? "Ideal review enabled. Use /ideal-off to disable it."
1926
+ : "Ideal review disabled. Use /ideal-on to enable it.",
2171
1927
  id: getId(),
2172
1928
  },
2173
1929
  ]);
2174
1930
  return;
2175
1931
  }
2176
- // Handle /clearplandismiss the approved plan
2177
- if (trimmed === "/clearplan") {
2178
- approvedPlanPathRef.current = undefined;
2179
- planStepsRef.current = [];
2180
- setPlanSteps([]);
2181
- // Rebuild system prompt without the plan
2182
- void replaceSystemPrompt({ clearApprovedPlan: true });
2183
- setLiveItems([{ kind: "plan_event", event: "dismissed", id: getId() }]);
2184
- return;
2185
- }
2186
- // Handle /map — show, refresh, or toggle dynamic repo map injection
2187
- if (trimmed === "/map" ||
2188
- trimmed === "/map refresh" ||
2189
- trimmed === "/map on" ||
2190
- trimmed === "/map off") {
2191
- const action = trimmed.slice("/map".length).trim();
2192
- if (action === "on") {
2193
- repoMapInjectionEnabledRef.current = true;
2194
- repoMapDirtyRef.current = true;
1932
+ // /rewindopen the checkpoint picker (needs React state + the store).
1933
+ if (trimmed === "/rewind") {
1934
+ const store = props.checkpointStore;
1935
+ if (!store) {
2195
1936
  setLiveItems((prev) => [
2196
1937
  ...prev,
2197
- { kind: "info", text: "Dynamic repo map injection is on.", id: getId() },
1938
+ { kind: "info", text: "Checkpoints are not available in this session.", id: getId() },
2198
1939
  ]);
2199
1940
  return;
2200
1941
  }
2201
- if (action === "off") {
2202
- repoMapInjectionEnabledRef.current = false;
2203
- messagesRef.current = stripRepoMapMessages(messagesRef.current);
1942
+ const cps = await store.listCheckpoints();
1943
+ if (cps.length === 0) {
2204
1944
  setLiveItems((prev) => [
2205
1945
  ...prev,
2206
1946
  {
2207
1947
  kind: "info",
2208
- text: "Dynamic repo map injection is off for this session.",
1948
+ text: "No checkpoints yet \u2014 edit a file through ezcoder first.",
2209
1949
  id: getId(),
2210
1950
  },
2211
1951
  ]);
2212
1952
  return;
2213
1953
  }
2214
- if (action === "refresh")
2215
- repoMapDirtyRef.current = true;
2216
- const markdown = await refreshRepoMap(getLatestUserText(messagesRef.current));
2217
- setLiveItems((prev) => [
2218
- ...prev,
2219
- {
2220
- kind: "info",
2221
- text: formatRepoMapCommandOutput(repoMapInjectionEnabledRef.current, markdown, action === "refresh"),
2222
- id: getId(),
2223
- },
2224
- ]);
1954
+ setRewindCheckpoints(cps);
2225
1955
  return;
2226
1956
  }
2227
- // Handle /goals — open goal pane
2228
- if (trimmed === "/goals") {
2229
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2230
- props.sessionStore.overlay = "goal";
2231
- props.sessionStore.planAutoExpand = false;
2232
- props.resetUI();
2233
- }
2234
- else {
2235
- if (props.sessionStore) {
2236
- props.sessionStore.overlay = "goal";
2237
- props.sessionStore.planAutoExpand = false;
2238
- if (agentLoop.isRunning)
2239
- props.sessionStore.pendingResetUI = true;
1957
+ if (await handleUiSlashCommand(trimmed, {
1958
+ openModelSelector: () => setOverlay("model"),
1959
+ compactConversation: async () => {
1960
+ const ac = new AbortController();
1961
+ compactionAbortRef.current = ac;
1962
+ const compacted = await compactConversation(messagesRef.current, ac.signal);
1963
+ if (!ac.signal.aborted && compacted !== messagesRef.current) {
1964
+ messagesRef.current = compacted;
1965
+ await persistCompactedSession(compacted);
2240
1966
  }
2241
- setPlanAutoExpand(false);
2242
- setOverlay("goal");
2243
- }
2244
- return;
2245
- }
2246
- // Handle /plans — open plan pane
2247
- if (trimmed === "/plans") {
2248
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2249
- props.sessionStore.overlay = "plan";
2250
- props.sessionStore.planAutoExpand = false;
2251
- props.resetUI();
2252
- }
2253
- else {
2254
- if (props.sessionStore) {
2255
- props.sessionStore.overlay = "plan";
2256
- props.sessionStore.planAutoExpand = false;
2257
- if (agentLoop.isRunning)
2258
- props.sessionStore.pendingResetUI = true;
1967
+ if (compactionAbortRef.current === ac)
1968
+ compactionAbortRef.current = null;
1969
+ },
1970
+ quit: showSessionSummaryAndExit,
1971
+ clearSession: () => {
1972
+ if (props.resetUI) {
1973
+ void (async () => {
1974
+ const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
1975
+ props.resetUI?.({
1976
+ wipeSession: true,
1977
+ messages: [{ role: "system", content: newPrompt }],
1978
+ });
1979
+ })();
1980
+ return;
2259
1981
  }
2260
- setPlanAutoExpand(false);
2261
- setOverlay("plan");
2262
- }
1982
+ clearPendingHistory();
1983
+ setHistory([{ kind: "banner", id: "banner" }]);
1984
+ setLiveItems([]);
1985
+ setDoneStatus(null);
1986
+ approvedPlanPathRef.current = undefined;
1987
+ planStepsRef.current = [];
1988
+ setPlanSteps([]);
1989
+ void (async () => {
1990
+ const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
1991
+ messagesRef.current = [{ role: "system", content: newPrompt }];
1992
+ persistedIndexRef.current = messagesRef.current.length;
1993
+ })();
1994
+ agentLoop.reset();
1995
+ setSessionTitle(undefined);
1996
+ sessionTitleGeneratedRef.current = false;
1997
+ setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
1998
+ },
1999
+ openThemeSelector: () => setOverlay("theme"),
2000
+ toggleMarkdown: () => {
2001
+ setRenderMarkdown((prev) => {
2002
+ const next = !prev;
2003
+ setLiveItems([
2004
+ {
2005
+ kind: "info",
2006
+ text: next ? "Rendered markdown mode." : "Raw markdown mode.",
2007
+ id: getId(),
2008
+ },
2009
+ ]);
2010
+ return next;
2011
+ });
2012
+ },
2013
+ clearApprovedPlan: () => {
2014
+ approvedPlanPathRef.current = undefined;
2015
+ planStepsRef.current = [];
2016
+ setPlanSteps([]);
2017
+ void replaceSystemPrompt({ clearApprovedPlan: true });
2018
+ setLiveItems([{ kind: "plan_event", event: "dismissed", id: getId() }]);
2019
+ },
2020
+ })) {
2263
2021
  return;
2264
2022
  }
2265
- // Handle prompt-template commands (built-in + custom from .ezcoder/commands/)
2266
- const promptCommandRoute = routePromptCommandInput(trimmed, PROMPT_COMMANDS, customCommands);
2267
- if (promptCommandRoute) {
2268
- const { cmdName, cmdArgs, fullPrompt } = promptCommandRoute;
2269
- log("INFO", "command", `Prompt command: /${cmdName}${cmdArgs ? ` (args: ${cmdArgs})` : ""}`);
2270
- // Move live items into history before starting
2271
- setLiveItems((prev) => {
2272
- if (prev.length > 0) {
2273
- pendingFlushRef.current = [...pendingFlushRef.current, ...prev];
2023
+ if (await submitPromptCommand({
2024
+ trimmed,
2025
+ inputImages,
2026
+ currentModel,
2027
+ customCommands,
2028
+ setLastUserMessage,
2029
+ setDoneStatus,
2030
+ finalizeSubmittedUserItem,
2031
+ runAgent: (content) => agentLoop.run(content),
2032
+ setLiveItems,
2033
+ getId,
2034
+ reloadCustomCommands,
2035
+ messagesRef,
2036
+ setGoalModeAndPrompt,
2037
+ onGoalStage: appendGoalAgentTransition,
2038
+ onGoalSetupComplete: () => {
2039
+ setGoalAutoExpand(true);
2040
+ if (props.sessionStore) {
2041
+ props.sessionStore.overlay = "goal";
2042
+ props.sessionStore.goalAutoExpand = true;
2274
2043
  }
2275
- return [];
2276
- });
2277
- const hasImages = inputImages.length > 0;
2278
- const modelInfo = getModel(currentModel);
2279
- const modelSupportsImages = modelInfo?.supportsImages ?? true;
2280
- const userContent = buildUserContentWithAttachments(fullPrompt, inputImages, modelSupportsImages);
2281
- // Show the typed command as the user message
2282
- const userItem = {
2283
- kind: "user",
2284
- text: trimmed,
2285
- imageCount: hasImages ? inputImages.length : undefined,
2286
- id: getId(),
2287
- };
2288
- setLastUserMessage(trimmed);
2289
- setDoneStatus(null);
2290
- setLiveItems([userItem]);
2291
- // Send the full prompt to the agent, with user args appended if provided
2292
- try {
2293
- await agentLoop.run(userContent);
2294
- }
2295
- catch (err) {
2296
- const msg = err instanceof Error ? err.message : String(err);
2297
- log("ERROR", "error", msg);
2298
- const isAbort = msg.includes("aborted") || msg.includes("abort");
2299
- setLiveItems((prev) => [
2300
- ...prev,
2301
- isAbort
2302
- ? { kind: "stopped", text: "Request was stopped.", id: getId() }
2303
- : toErrorItem(err, getId()),
2304
- ]);
2305
- }
2306
- // Reload custom commands in case a setup command created new ones
2307
- reloadCustomCommands();
2044
+ setOverlay("goal");
2045
+ },
2046
+ })) {
2308
2047
  return;
2309
2048
  }
2310
2049
  // Check slash commands
@@ -2323,7 +2062,7 @@ export function App(props) {
2323
2062
  // ── Queue message if agent is already running ──
2324
2063
  if (agentLoop.isRunning) {
2325
2064
  log("INFO", "queue", `Queued message: ${trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed}`);
2326
- agentLoop.queueMessage(userContent);
2065
+ agentLoop.queueMessage(userContent, input);
2327
2066
  let displayText = input;
2328
2067
  if (hasImages) {
2329
2068
  const { cleanText } = await extractImagePaths(input, props.cwd);
@@ -2338,31 +2077,32 @@ export function App(props) {
2338
2077
  setLiveItems((prev) => [...prev, queuedItem]);
2339
2078
  return;
2340
2079
  }
2341
- // Move any remaining live items into history (Static) before starting a
2342
- // new turn. Must go through queueFlush so flushGeneration bumps and the
2343
- // drain effect actually runs — mutating pendingFlushRef directly here
2344
- // stashed items that nothing was signalled to pick up, so they sat in
2345
- // limbo until some unrelated later code path happened to call queueFlush.
2346
- setLiveItems((prev) => {
2347
- if (prev.length > 0) {
2348
- queueFlush(prev);
2349
- }
2350
- return [];
2351
- });
2352
2080
  // Build display text — strip image paths, show badges instead
2353
2081
  let displayText = input;
2354
2082
  if (hasImages) {
2355
2083
  const { cleanText } = await extractImagePaths(input, props.cwd);
2356
2084
  displayText = cleanText;
2357
2085
  }
2086
+ let imagePreviews;
2087
+ if (hasImages) {
2088
+ const built = await Promise.all(inputImages
2089
+ .filter((img) => img.kind === "image")
2090
+ .map(async (img) => {
2091
+ const downscaled = await downscaleForPreview(Buffer.from(img.data, "base64"));
2092
+ return { base64: downscaled.toString("base64"), mediaType: img.mediaType };
2093
+ }));
2094
+ imagePreviews = built.length > 0 ? built : undefined;
2095
+ }
2358
2096
  const userItem = {
2359
2097
  kind: "user",
2360
2098
  text: displayText,
2361
2099
  imageCount: hasImages ? inputImages.length : undefined,
2100
+ imagePreviews,
2362
2101
  pasteInfo,
2363
2102
  id: getId(),
2364
2103
  };
2365
2104
  setLastUserMessage(input);
2105
+ setScrollResetToken((token) => token + 1);
2366
2106
  setDoneStatus(null);
2367
2107
  // Clear stale plan progress if there's no active approved plan
2368
2108
  // (avoids lingering progress from a completed or abandoned plan run)
@@ -2370,7 +2110,18 @@ export function App(props) {
2370
2110
  planStepsRef.current = [];
2371
2111
  setPlanSteps([]);
2372
2112
  }
2373
- setLiveItems([userItem]);
2113
+ finalizeSubmittedUserItem(userItem, liveItems);
2114
+ // Open a per-turn checkpoint capturing the conversation position right
2115
+ // before this exchange, so /rewind can restore pre-turn code/chat state.
2116
+ if (props.checkpointStore) {
2117
+ rewindTurnRef.current += 1;
2118
+ await props.checkpointStore
2119
+ .openCheckpoint({
2120
+ turnIndex: rewindTurnRef.current,
2121
+ messageIndex: messagesRef.current.length,
2122
+ })
2123
+ .catch(() => { });
2124
+ }
2374
2125
  // Run agent
2375
2126
  try {
2376
2127
  await agentLoop.run(userContent);
@@ -2388,17 +2139,29 @@ export function App(props) {
2388
2139
  }
2389
2140
  }, [
2390
2141
  agentLoop,
2391
- props.onSlashCommand,
2392
2142
  compactConversation,
2143
+ currentModel,
2144
+ finalizeSubmittedUserItem,
2145
+ liveItems,
2146
+ props.cwd,
2147
+ props.onSlashCommand,
2148
+ props.resetUI,
2149
+ props.sessionStore,
2393
2150
  rebuildSystemPrompt,
2151
+ showSessionSummaryAndExit,
2152
+ reloadCustomCommands,
2394
2153
  replaceSystemPrompt,
2395
- refreshRepoMap,
2396
- stripRepoMapMessages,
2397
2154
  ]);
2398
- const handleDoubleExit = useDoublePress(setExitPending, () => process.exit(0));
2155
+ const handleDoubleExit = useDoublePress(setExitPending, showSessionSummaryAndExit);
2399
2156
  const handleAbort = useCallback(() => {
2400
2157
  if (agentLoop.isRunning) {
2401
- agentLoop.clearQueue();
2158
+ // Restore any unsent queued messages to the composer instead of dropping
2159
+ // them, so an interrupt never silently discards what the user typed.
2160
+ const queuedText = agentLoop.drainQueuedText();
2161
+ if (queuedText) {
2162
+ setLiveItems((prev) => prev.filter((item) => item.kind !== "queued"));
2163
+ setComposerInject({ text: queuedText, nonce: nextIdRef.current++ });
2164
+ }
2402
2165
  agentLoop.abort();
2403
2166
  }
2404
2167
  else if (compactionAbortRef.current) {
@@ -2407,22 +2170,22 @@ export function App(props) {
2407
2170
  else {
2408
2171
  handleDoubleExit();
2409
2172
  }
2410
- }, [agentLoop, handleDoubleExit]);
2173
+ }, [agentLoop, handleDoubleExit, setLiveItems]);
2411
2174
  const handleToggleThinking = useCallback(() => {
2412
- setThinkingEnabled((prev) => {
2413
- const next = !prev;
2414
- log("INFO", "thinking", `Thinking ${next ? "enabled" : "disabled"}`);
2415
- setLiveItems((items) => [
2416
- ...items,
2417
- { kind: "thinking_transition", active: next, id: getId() },
2418
- ]);
2175
+ setThinkingLevel((prev) => {
2176
+ const next = getNextThinkingLevel(currentProvider, currentModel, prev);
2177
+ log("INFO", "thinking", next ? `Thinking ${next}` : "Thinking disabled");
2419
2178
  if (props.settingsFile) {
2420
2179
  const sm = new SettingsManager(props.settingsFile);
2421
- sm.load().then(() => sm.set("thinkingEnabled", next));
2180
+ void sm.load().then(async () => {
2181
+ await sm.set("thinkingEnabled", !!next);
2182
+ if (next)
2183
+ await sm.set("thinkingLevel", next);
2184
+ });
2422
2185
  }
2423
2186
  return next;
2424
2187
  });
2425
- }, [props.settingsFile]);
2188
+ }, [currentProvider, currentModel, props.settingsFile]);
2426
2189
  const handleModelSelect = useCallback((value) => {
2427
2190
  setOverlay(null);
2428
2191
  const colonIdx = value.indexOf(":");
@@ -2431,6 +2194,9 @@ export function App(props) {
2431
2194
  const newProvider = value.slice(0, colonIdx);
2432
2195
  const newModelId = value.slice(colonIdx + 1);
2433
2196
  log("INFO", "model", `Model changed`, { provider: newProvider, model: newModelId });
2197
+ // Keep the ref in sync before any prompt rebuild so the identity (Claude
2198
+ // Code vs EZ Coder) reflects the newly selected provider immediately.
2199
+ currentProviderRef.current = newProvider;
2434
2200
  const rebuildPromptWithTools = (tools) => {
2435
2201
  currentToolsRef.current = tools;
2436
2202
  void replaceSystemPrompt({ tools });
@@ -2530,214 +2296,179 @@ export function App(props) {
2530
2296
  const promptByName = new Map(PROMPT_COMMANDS.map((c) => [c.name, c]));
2531
2297
  const fromPrompt = (name) => {
2532
2298
  const c = promptByName.get(name);
2533
- return c ? { name: c.name, aliases: c.aliases, description: c.description } : null;
2299
+ return c
2300
+ ? {
2301
+ name: c.name,
2302
+ aliases: c.aliases,
2303
+ description: c.description,
2304
+ sectionTitle: "workflows",
2305
+ }
2306
+ : null;
2534
2307
  };
2535
2308
  const promptOrder = [
2536
2309
  // Project audits / one-shot analysis
2537
- "goal",
2538
2310
  "init",
2539
- "research",
2540
- "scan",
2541
- "verify",
2542
2311
  "expand",
2543
2312
  "bullet-proof",
2544
- "simplify",
2545
2313
  "compare",
2546
- "batch",
2547
2314
  // Setup / installers
2548
- "setup-lint",
2549
- "setup-tests",
2550
2315
  "setup-commit",
2551
- "setup-update",
2552
- "setup-eyes",
2553
- "eyes-improve",
2554
2316
  "setup-skills",
2555
2317
  ];
2556
2318
  const orderedPromptCommands = promptOrder
2557
2319
  .map(fromPrompt)
2558
2320
  .filter((c) => c !== null);
2559
2321
  const knownPromptNames = new Set(promptOrder);
2560
- const remainingPromptCommands = PROMPT_COMMANDS.filter((c) => !knownPromptNames.has(c.name)).map((c) => ({ name: c.name, aliases: c.aliases, description: c.description }));
2322
+ const remainingPromptCommands = PROMPT_COMMANDS.filter((c) => !knownPromptNames.has(c.name)).map((c) => ({
2323
+ name: c.name,
2324
+ aliases: c.aliases,
2325
+ description: c.description,
2326
+ sectionTitle: "workflows",
2327
+ }));
2561
2328
  return [
2562
2329
  // Session actions (most frequent)
2563
- { name: "model", aliases: ["m"], description: "Switch model" },
2564
- { name: "compact", aliases: ["c"], description: "Compact conversation" },
2565
- { name: "clear", aliases: [], description: "Clear session and terminal" },
2566
- { name: "theme", aliases: ["t"], description: "Switch theme" },
2567
- { name: "plans", aliases: [], description: "Open plans pane" },
2330
+ { name: "model", aliases: ["m"], description: "Switch model", sectionTitle: "built-in" },
2331
+ { name: "compact", aliases: ["c"], description: "Compact context", sectionTitle: "built-in" },
2332
+ { name: "clear", aliases: [], description: "Clear session", sectionTitle: "built-in" },
2333
+ { name: "theme", aliases: ["t"], description: "Switch theme", sectionTitle: "built-in" },
2334
+ {
2335
+ name: idealReviewEnabled ? "ideal-off" : "ideal-on",
2336
+ aliases: [],
2337
+ description: idealReviewEnabled
2338
+ ? "Disable pre-final ideal review"
2339
+ : "Enable pre-final ideal review",
2340
+ sectionTitle: "built-in",
2341
+ },
2342
+ {
2343
+ name: "rewind",
2344
+ aliases: [],
2345
+ description: "Restore files/conversation to a checkpoint",
2346
+ sectionTitle: "built-in",
2347
+ },
2568
2348
  ...orderedPromptCommands,
2569
2349
  ...remainingPromptCommands,
2570
2350
  ...customCommands.map((cmd) => ({
2571
2351
  name: cmd.name,
2572
2352
  aliases: [],
2573
2353
  description: cmd.description,
2354
+ sectionTitle: "custom",
2574
2355
  })),
2575
- { name: "quit", aliases: ["q", "exit"], description: "Exit the agent" },
2356
+ {
2357
+ name: "quit",
2358
+ aliases: ["q", "exit"],
2359
+ description: "Exit ezcoder",
2360
+ sectionTitle: "built-in",
2361
+ },
2576
2362
  ];
2577
- }, [customCommands]);
2578
- const renderItem = (item) => {
2579
- switch (item.kind) {
2580
- case "tombstone":
2581
- return null;
2582
- case "banner":
2583
- return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd, taskCount: taskCount, goalCount: goalCount }, item.id));
2584
- case "user":
2585
- return (_jsx(UserMessage, { text: item.text, imageCount: item.imageCount, pasteInfo: item.pasteInfo }, item.id));
2586
- case "task":
2587
- return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "▶ " }), _jsx(Text, { color: theme.textDim, children: "Task: " }), _jsx(Text, { color: theme.success, children: item.title })] }) }, item.id));
2588
- case "goal":
2589
- return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "▶ " }), _jsx(Text, { color: theme.textDim, children: "Goal: " }), _jsx(Text, { color: theme.success, children: item.title }), item.workerId ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] }) : null] }) }, item.id));
2590
- case "goal_progress": {
2591
- const isError = item.status === "failed" || item.status === "fail" || item.status === "blocked";
2592
- const color = isError
2593
- ? theme.error
2594
- : item.phase === "worker_finished"
2595
- ? theme.success
2596
- : item.phase === "verifier_finished"
2597
- ? theme.accent
2598
- : item.phase === "orchestrator_reviewing" || item.phase === "orchestrator_working"
2599
- ? theme.secondary
2600
- : item.phase === "continuing"
2601
- ? theme.warning
2602
- : item.phase === "verifier_started"
2603
- ? theme.accent
2604
- : item.phase === "worker_started"
2605
- ? theme.primary
2606
- : item.phase === "terminal"
2607
- ? theme.success
2608
- : theme.primary;
2609
- const glyph = item.phase === "worker_finished" || item.phase === "verifier_finished"
2610
- ? "✓ "
2611
- : item.phase === "terminal"
2612
- ? item.status === "passed"
2613
- ? "◆ "
2614
- : "! "
2615
- : "↻ ";
2616
- return (_jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: color, bold: true, children: glyph }), _jsx(Text, { color: color, bold: true, children: item.title }), item.workerId ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] }) : null] }), item.detail ? (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` ${item.detail}` })) : null, item.summaryRows && item.summaryRows.length > 0 ? (_jsx(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, flexShrink: 1, children: item.summaryRows.map((row) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.textDim, children: row.label.padEnd(10) }), _jsx(Text, { color: theme.text, children: row.value }), row.detail ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 ", row.detail] }) : null] }, row.label))) })) : null] }, item.id));
2617
- }
2618
- case "style_pack": {
2619
- const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
2620
- const headerLabel = item.added.length > 1 ? "STYLE PACKS ACTIVE" : "STYLE PACK ACTIVE";
2621
- return (_jsxs(Box, { marginTop: 1, flexShrink: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.language, paddingX: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.language, bold: true, children: "◆ " }), _jsx(Text, { color: theme.language, bold: true, children: headerLabel })] }), _jsx(Text, { color: theme.text, bold: true, wrap: "wrap", children: names.join(", ") }), item.showSetupHint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.textMuted, children: "Tip: run " }), _jsx(Text, { color: theme.language, bold: true, children: "/setup" }), _jsx(Text, { color: theme.textMuted, children: " to audit this project against the active pack(s)" })] }) }))] }, item.id));
2622
- }
2623
- case "setup_hint":
2624
- return (_jsxs(Box, { marginTop: 1, flexShrink: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.language, paddingX: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.language, bold: true, children: "◆ " }), _jsx(Text, { color: theme.language, bold: true, children: "NO STYLE PACKS DETECTED" })] }), _jsx(Text, { color: theme.textMuted, wrap: "wrap", children: "This directory has no recognized language manifest at its root." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.textMuted, children: "Tip: run " }), _jsx(Text, { color: theme.language, bold: true, children: "/setup" }), _jsx(Text, { color: theme.textMuted, children: " to audit project hygiene or bootstrap a new project from scratch" })] }) })] }, item.id));
2625
- case "assistant":
2626
- return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, planMode: item.planMode }, item.id));
2627
- case "tool_start":
2628
- return (_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput, animateUntil: item.animateUntil }, item.id));
2629
- case "tool_done":
2630
- return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
2631
- case "tool_group":
2632
- return _jsx(ToolGroupExecution, { tools: item.tools }, item.id);
2633
- case "server_tool_start":
2634
- return (_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt, animateUntil: item.animateUntil }, item.id));
2635
- case "server_tool_done":
2636
- return (_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
2637
- case "error": {
2638
- const showMessage = item.message && item.message !== item.headline;
2639
- return (_jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 1, children: [_jsxs(Text, { color: theme.error, wrap: "wrap", children: ["✗ ", item.headline] }), showMessage && (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` ${item.message}` })), _jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` → ${item.guidance}` })] }, item.id));
2640
- }
2641
- case "info":
2642
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.text }) }, item.id));
2643
- case "update_notice":
2644
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, borderStyle: "round", borderColor: theme.success, paddingX: 1, children: _jsxs(Text, { color: theme.success, bold: true, wrap: "wrap", children: ["✨ ", item.text] }) }, item.id));
2645
- case "plan_transition":
2646
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.planPrimary, bold: true, wrap: "wrap", children: [item.active ? "● " : "● ", item.text] }) }, item.id));
2647
- case "thinking_transition": {
2648
- const glyphColor = item.active ? THINKING_BORDER_COLORS[0] : theme.textDim;
2649
- return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: glyphColor, bold: true, children: "✻ " }), _jsx(Text, { color: item.active ? theme.accent : theme.textDim, bold: true, children: item.active ? "Thinking ON" : "Thinking OFF" })] }, item.id));
2650
- }
2651
- case "model_transition": {
2652
- const glyphColor = THINKING_BORDER_COLORS[0];
2653
- return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: glyphColor, bold: true, children: "▸ " }), _jsx(Text, { color: theme.textDim, children: "Switched to " }), _jsx(Text, { color: theme.primary, bold: true, children: item.modelName })] }, item.id));
2654
- }
2655
- case "theme_transition": {
2656
- const glyphColor = THINKING_BORDER_COLORS[0];
2657
- return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: glyphColor, bold: true, children: "◐ " }), _jsx(Text, { color: theme.textDim, children: "Theme switched to " }), _jsx(Text, { color: theme.primary, bold: true, children: item.themeName })] }, item.id));
2658
- }
2659
- case "plan_event": {
2660
- // Plan-domain status changes (approve / reject / dismiss). Uses
2661
- // theme.planPrimary to match the existing plan_transition family,
2662
- // distinct from the model/thinking gradient.
2663
- const label = item.event === "approved"
2664
- ? "Plan approved"
2665
- : item.event === "rejected"
2666
- ? "Plan rejected"
2667
- : "Plan dismissed";
2668
- return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsxs(Text, { color: theme.planPrimary, bold: true, children: ["○ ", label] }), item.detail ? _jsx(Text, { color: theme.textDim, children: ` — "${item.detail}"` }) : null] }, item.id));
2669
- }
2670
- case "stopped":
2671
- // Cancellation / abort acknowledgement (ESC, auto-setup cancel, etc.).
2672
- // Muted dim treatment — this is an ack, not a state change worth a
2673
- // gradient. Glyph `⊘` reads as "stop" without being alarming.
2674
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.textDim, bold: true, children: ["⊘ ", item.text] }) }, item.id));
2675
- case "step_done":
2676
- return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "✓ " }), _jsx(Text, { color: theme.success, bold: true, children: `Step ${item.stepNum} done` }), item.description ? (_jsx(Text, { color: theme.textDim, children: ` — ${item.description}` })) : null] }) }, item.id));
2677
- case "queued":
2678
- return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: "⏳ Queued: " }), _jsxs(Text, { color: theme.text, wrap: "wrap", children: [item.text, item.imageCount
2679
- ? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
2680
- : ""] })] }, item.id));
2681
- case "compacting":
2682
- return _jsx(CompactionSpinner, { staticDisplay: true }, item.id);
2683
- case "compacted":
2684
- return (_jsx(CompactionDone, { originalCount: item.originalCount, newCount: item.newCount, tokensBefore: item.tokensBefore, tokensAfter: item.tokensAfter }, item.id));
2685
- case "duration":
2686
- return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.textDim, children: ["✻ ", item.verb, " ", formatDuration(item.durationMs)] }) }, item.id));
2687
- case "subagent_group":
2688
- return _jsx(SubAgentPanel, { agents: item.agents, aborted: item.aborted }, item.id);
2363
+ }, [customCommands, idealReviewEnabled]);
2364
+ const renderItem = (item, index, items) => renderTranscriptItem({
2365
+ item,
2366
+ index,
2367
+ items,
2368
+ pendingHistoryFlushLastItem: index === 0 ? lastVisibleTranscriptItem(pendingHistoryFlushRef.current) : undefined,
2369
+ historyLastItem: index === 0 ? lastVisibleTranscriptItem(history) : undefined,
2370
+ version: props.version,
2371
+ currentModel,
2372
+ currentProvider,
2373
+ displayedCwd,
2374
+ columns,
2375
+ theme,
2376
+ renderMarkdown,
2377
+ measuredLiveAreaRows,
2378
+ });
2379
+ const openOverlay = useCallback((kind) => {
2380
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2381
+ props.sessionStore.overlay = kind;
2382
+ if (kind !== "plan")
2383
+ props.sessionStore.planAutoExpand = false;
2384
+ if (kind !== "goal")
2385
+ props.sessionStore.goalAutoExpand = false;
2386
+ props.resetUI();
2689
2387
  }
2690
- };
2691
- // ── Start a task (shared by manual "work on it" and run-all) ──
2692
- const startTask = useCallback((title, prompt, taskId) => {
2693
- setTaskCount(getTaskCount(props.cwd));
2694
- const shortId = taskId.slice(0, 8);
2695
- const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
2696
- `tasks({ action: "done", id: "${shortId}" })`;
2697
- const fullPrompt = prompt + completionHint;
2698
- // Task-pane runs are unattended — there is no human to approve a
2699
- // plan via the plan overlay. Force plan mode off and flag
2700
- // `taskRunningRef` so `enter_plan` refuses if the agent tries to call
2701
- // it. Both flags persist across the resetUI() unmount/remount below
2702
- // because they live in cli.ts-owned refs, not React state.
2703
- if (props.planModeRef)
2704
- props.planModeRef.current = false;
2705
- planModeStateRef.current = false;
2706
- approvedPlanPathRef.current = undefined;
2707
- setPlanMode(false);
2708
- if (props.taskRunningRef)
2709
- props.taskRunningRef.current = true;
2710
- if (props.resetUI && props.sessionStore) {
2711
- const taskItem = { kind: "task", title, id: String(nextIdRef.current++) };
2712
- const sm = sessionManagerRef.current;
2713
- void (async () => {
2714
- // Rebuild the system prompt without plan mode / approved plan so the
2715
- // next mount doesn't inherit a stale plan-mode prefix from a prior
2716
- // user-toggled state. Skills, languages, and cwd are preserved.
2717
- const taskSysPrompt = await rebuildSystemPrompt({
2718
- planMode: false,
2719
- clearApprovedPlan: true,
2720
- });
2721
- const newMessages = [{ role: "system", content: taskSysPrompt }];
2388
+ else {
2389
+ if (props.sessionStore) {
2390
+ props.sessionStore.overlay = kind;
2391
+ if (kind !== "plan")
2392
+ props.sessionStore.planAutoExpand = false;
2393
+ if (kind !== "goal")
2394
+ props.sessionStore.goalAutoExpand = false;
2395
+ if (agentLoop.isRunning && kind !== "plan") {
2396
+ props.sessionStore.pendingResetUI = true;
2397
+ }
2398
+ }
2399
+ if (kind !== "plan")
2400
+ setPlanAutoExpand(false);
2401
+ if (kind !== "goal")
2402
+ setGoalAutoExpand(false);
2403
+ setOverlay(kind);
2404
+ }
2405
+ }, [agentLoop.isRunning, props]);
2406
+ useEffect(() => {
2407
+ runAllTasksRef.current = runAllTasks;
2408
+ if (props.sessionStore)
2409
+ props.sessionStore.runAllTasks = runAllTasks;
2410
+ }, [runAllTasks, props.sessionStore]);
2411
+ useEffect(() => {
2412
+ agentRunningRef.current = agentLoop.isRunning;
2413
+ }, [agentLoop.isRunning]);
2414
+ const { startPixelFix, setRunAllPixel } = usePixelFixFlow({
2415
+ agentLoop,
2416
+ cwd: props.cwd,
2417
+ currentProvider,
2418
+ currentModel,
2419
+ rebuildToolsForCwd: props.rebuildToolsForCwd,
2420
+ sessionStore: props.sessionStore,
2421
+ currentPixelFixRef,
2422
+ runAllPixelRef,
2423
+ startPixelFixRef,
2424
+ cwdRef,
2425
+ currentToolsRef,
2426
+ injectedLanguagesRef,
2427
+ setupHintShownRef,
2428
+ messagesRef,
2429
+ persistedIndexRef,
2430
+ sessionManagerRef,
2431
+ sessionPathRef,
2432
+ setDisplayedCwd,
2433
+ setCurrentTools,
2434
+ setHistory,
2435
+ setLiveItems,
2436
+ setLastUserMessage,
2437
+ setDoneStatus,
2438
+ rebuildSystemPrompt,
2439
+ clearPendingHistory,
2440
+ getId,
2441
+ initialRunAllPixel: props.sessionStore?.runAllPixel ?? false,
2442
+ });
2443
+ // Starts a single task: opens a fresh session + chat and runs the task
2444
+ // prompt through the agent loop. Wired into startTaskRef so both the task
2445
+ // picker (Enter = start one, r = run all) and the run-all auto-advance in
2446
+ // onDone can invoke it from stale closures.
2447
+ const startTask = useCallback((title, prompt, taskId) => {
2448
+ const taskCwd = cwdRef.current;
2449
+ const shortId = taskId.slice(0, 8);
2450
+ const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
2451
+ `tasks({ action: "done", id: "${shortId}" })`;
2452
+ const fullPrompt = prompt + completionHint;
2453
+ if (props.resetUI && props.sessionStore) {
2454
+ const sysMsg = messagesRef.current[0];
2455
+ const newMessages = sysMsg && sysMsg.role === "system" ? [sysMsg] : messagesRef.current.slice(0, 1);
2456
+ const taskItem = { kind: "task", title, id: getId() };
2457
+ const sm = sessionManagerRef.current;
2458
+ void (async () => {
2722
2459
  let newSessionPath;
2723
2460
  if (sm) {
2724
2461
  try {
2725
- const s = await sm.create(props.cwd, currentProvider, currentModel);
2726
- newSessionPath = s.path;
2727
- log("INFO", "tasks", "New session for task", { path: s.path });
2462
+ const session = await sm.create(taskCwd, currentProvider, currentModel);
2463
+ newSessionPath = session.path;
2464
+ log("INFO", "tasks", "New session for task", { path: session.path });
2728
2465
  }
2729
2466
  catch {
2730
- // session creation is best-effort
2467
+ // Session creation is best-effort.
2731
2468
  }
2732
2469
  }
2733
- if (props.sessionStore) {
2470
+ if (props.sessionStore)
2734
2471
  props.sessionStore.overlay = null;
2735
- // Mirror through sessionStore so the post-resetUI remount can
2736
- // re-seed the ref. markTaskDone() in the run-all onDone branch
2737
- // reads currentTaskIdRef.current, which is seeded from this.
2738
- props.sessionStore.currentTaskId = taskId;
2739
- }
2740
- currentTaskIdRef.current = taskId;
2741
2472
  props.resetUI?.({
2742
2473
  wipeSession: true,
2743
2474
  messages: newMessages,
@@ -2748,10 +2479,7 @@ export function App(props) {
2748
2479
  })();
2749
2480
  return;
2750
2481
  }
2751
- // Fallback path (resetUI not wired — tests).
2752
- currentTaskIdRef.current = taskId;
2753
- if (props.sessionStore)
2754
- props.sessionStore.currentTaskId = taskId;
2482
+ clearPendingHistory();
2755
2483
  setHistory([{ kind: "banner", id: "banner" }]);
2756
2484
  setLiveItems([]);
2757
2485
  messagesRef.current = messagesRef.current.slice(0, 1);
@@ -2759,940 +2487,585 @@ export function App(props) {
2759
2487
  persistedIndexRef.current = messagesRef.current.length;
2760
2488
  const sm = sessionManagerRef.current;
2761
2489
  if (sm) {
2762
- void sm.create(props.cwd, currentProvider, currentModel).then((s) => {
2763
- sessionPathRef.current = s.path;
2764
- log("INFO", "tasks", "New session for task", { path: s.path });
2490
+ void sm.create(taskCwd, currentProvider, currentModel).then((session) => {
2491
+ sessionPathRef.current = session.path;
2492
+ log("INFO", "tasks", "New session for task", { path: session.path });
2765
2493
  });
2766
2494
  }
2767
2495
  const taskItem = { kind: "task", title, id: getId() };
2768
2496
  setLastUserMessage(title);
2769
2497
  setDoneStatus(null);
2770
2498
  setLiveItems([taskItem]);
2499
+ void agentLoop.run(fullPrompt).catch((err) => {
2500
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
2501
+ });
2502
+ }, [agentLoop, currentModel, currentProvider, props]);
2503
+ // Keep the ref in sync so stale closures (task picker, onDone run-all) can
2504
+ // start a task without being recreated each render.
2505
+ startTaskRef.current = startTask;
2506
+ // Reset the live tool feed at the start of each run so the pinned panel only
2507
+ // ever reflects the current turn's activity, not the previous one's.
2508
+ const wasRunningRef = useRef(false);
2509
+ useEffect(() => {
2510
+ if (agentLoop.isRunning && !wasRunningRef.current) {
2511
+ setLiveToolFeed([]);
2512
+ }
2513
+ wasRunningRef.current = agentLoop.isRunning;
2514
+ }, [agentLoop.isRunning]);
2515
+ const isSkillsView = overlay === "skills";
2516
+ const isPlanView = overlay === "plan";
2517
+ const { footerStatusLayout, activityVisible, stallStatusVisible, statusSlotVisible, mainControlsRef, measuredLiveAreaRows, viewportRows, } = useChatLayoutMeasurements({
2518
+ rows,
2519
+ columns,
2520
+ backgroundTaskCount: bgTasks.length,
2521
+ updatePending,
2522
+ agentRunning: agentLoop.isRunning,
2523
+ activityPhase: agentLoop.activityPhase,
2524
+ stallError: agentLoop.stallError,
2525
+ doneStatus,
2526
+ currentModel,
2527
+ contextUsed: agentLoop.contextUsed,
2528
+ contextWindowOptions,
2529
+ displayedCwd,
2530
+ gitBranch,
2531
+ thinkingLevel,
2532
+ exitPending,
2533
+ taskBarExpanded,
2534
+ liveToolFeedCount: liveToolFeed.length,
2535
+ });
2536
+ const isPixelView = overlay === "pixel";
2537
+ const hasLiveAssistantItem = liveItems.some((item) => item.kind === "assistant");
2538
+ const rawVisibleStreamingText = hasLiveAssistantItem ? "" : agentLoop.streamingText;
2539
+ // Compute the prospective paragraph flush DURING render so the live frame
2540
+ // immediately drops the prefix that is about to be written to scrollback.
2541
+ // The queueFlush below runs in an effect (after paint), so if the live text
2542
+ // were sliced only by the already-committed `flushedChars`, the just-flushed
2543
+ // paragraph would render BOTH in scrollback and live for one frame — that
2544
+ // transient extra height is what shoves the footer up and then back down on
2545
+ // every chunk boundary. Slicing by the prospective flush here keeps the live
2546
+ // frame height monotonic, so the footer never bounces.
2547
+ const alreadyFlushedChars = streamedAssistantFlushRef.current.flushedChars;
2548
+ const pendingFlushChars = rawVisibleStreamingText
2549
+ ? splitAssistantStreamingText(rawVisibleStreamingText.slice(alreadyFlushedChars)).flushedText
2550
+ .length
2551
+ : 0;
2552
+ useEffect(() => {
2553
+ if (!rawVisibleStreamingText) {
2554
+ streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
2555
+ return;
2556
+ }
2557
+ if (rawVisibleStreamingText === streamedAssistantFlushRef.current.text)
2558
+ return;
2559
+ const alreadyFlushed = streamedAssistantFlushRef.current.flushedChars;
2560
+ const unflushedText = rawVisibleStreamingText.slice(alreadyFlushed);
2561
+ const split = splitAssistantStreamingText(unflushedText);
2562
+ if (split.flushedText.length > 0) {
2563
+ queueFlush([
2564
+ {
2565
+ kind: "assistant",
2566
+ text: stripDoneMarkers(split.flushedText),
2567
+ continuation: streamedAssistantFlushRef.current.flushedChars > 0,
2568
+ id: getId(),
2569
+ },
2570
+ ]);
2571
+ streamedAssistantFlushRef.current = {
2572
+ flushedChars: alreadyFlushed + split.flushedText.length,
2573
+ text: rawVisibleStreamingText,
2574
+ };
2575
+ return;
2576
+ }
2577
+ streamedAssistantFlushRef.current = {
2578
+ ...streamedAssistantFlushRef.current,
2579
+ text: rawVisibleStreamingText,
2580
+ };
2581
+ }, [rawVisibleStreamingText, queueFlush]);
2582
+ const visibleStreamingText = stripDoneMarkers(rawVisibleStreamingText.slice(alreadyFlushedChars + pendingFlushChars));
2583
+ const lastLiveItem = liveItems.at(-1);
2584
+ // For spacing decisions, the previous row is the last item that actually
2585
+ // RENDERS. Panel-replaced tool items (now shown only in the LiveToolPanel)
2586
+ // render null, so counting them as the boundary inserts a blank separator
2587
+ // above the streamed response with nothing visible above it.
2588
+ const lastVisibleLiveItem = lastVisibleTranscriptItem(liveItems);
2589
+ const lastPendingHistoryItem = pendingHistoryFlushRef.current.at(-1);
2590
+ const lastHistoryItem = history.at(-1);
2591
+ // Spacing variants: flushed tool rows render null (LiveToolPanel owns them), so
2592
+ // the streamed/first-live boundary must look past them to the last row that
2593
+ // actually printed — otherwise a tool→assistant separator leaves a phantom gap.
2594
+ const lastVisiblePendingHistoryItem = lastVisibleTranscriptItem(pendingHistoryFlushRef.current);
2595
+ const lastVisibleHistoryItem = lastVisibleTranscriptItem(history);
2596
+ const previousTranscriptItem = lastPendingHistoryItem ?? lastHistoryItem;
2597
+ const isAwaitingAssistantAfterUser = agentLoop.isRunning &&
2598
+ !hasLiveAssistantItem &&
2599
+ visibleStreamingText.trim().length === 0 &&
2600
+ (lastLiveItem?.kind === "user" || (!lastLiveItem && previousTranscriptItem?.kind === "user"));
2601
+ const shouldReserveStreamingSpacing = agentLoop.isRunning &&
2602
+ !hasLiveAssistantItem &&
2603
+ (visibleStreamingText.trim().length > 0 ||
2604
+ liveItems.some(isTranscriptSpacingItem) ||
2605
+ isAwaitingAssistantAfterUser);
2606
+ const shouldTopSpaceStreamingText = shouldTopSpaceStreamingAssistant({
2607
+ visibleStreamingText,
2608
+ lastLiveItem: lastVisibleLiveItem,
2609
+ lastPendingHistoryItem: lastVisiblePendingHistoryItem,
2610
+ lastHistoryItem: lastVisibleHistoryItem,
2611
+ });
2612
+ // When earlier paragraphs of THIS response were already flushed to scrollback
2613
+ // mid-stream, the live remainder is the next paragraph — re-insert the blank
2614
+ // line that separated them so the live tail lines up with the flushed history.
2615
+ const streamingContinuesFlushed = alreadyFlushedChars + pendingFlushChars > 0;
2616
+ // ── Fullscreen alt-screen transcript ───────────────────
2617
+ // Flatten history + live items + in-flight streaming into the flat ANSI line
2618
+ // buffer the viewport renders. Reuses the same serializer the legacy
2619
+ // scrollback printer used, so the transcript looks identical. Only computed
2620
+ // when fullscreen is active (the legacy path renders items through Ink).
2621
+ const transcriptContext = useMemo(() => ({
2622
+ theme,
2623
+ columns,
2624
+ version: props.version,
2625
+ model: currentModel,
2626
+ provider: currentProvider,
2627
+ cwd: displayedCwd,
2628
+ }), [theme, columns, props.version, currentModel, currentProvider, displayedCwd]);
2629
+ const transcriptLines = useMemo(() => {
2630
+ if (!props.fullscreen)
2631
+ return [];
2632
+ const items = [...history, ...uniqueItemsById(liveItems)];
2633
+ const hasStreaming = visibleStreamingText.length > 0 || agentLoop.streamingThinking.length > 0;
2634
+ if (hasStreaming) {
2635
+ items.push({
2636
+ kind: "assistant",
2637
+ text: visibleStreamingText,
2638
+ thinking: agentLoop.streamingThinking,
2639
+ thinkingMs: agentLoop.thinkingMs,
2640
+ continuation: streamingContinuesFlushed,
2641
+ id: "__streaming__",
2642
+ });
2643
+ }
2644
+ return buildTranscriptLines(items, transcriptContext);
2645
+ }, [
2646
+ props.fullscreen,
2647
+ history,
2648
+ liveItems,
2649
+ visibleStreamingText,
2650
+ agentLoop.streamingThinking,
2651
+ agentLoop.thinkingMs,
2652
+ streamingContinuesFlushed,
2653
+ transcriptContext,
2654
+ ]);
2655
+ // Keyboard + bounds controller. The offset itself lives in the external
2656
+ // transcript-scroll store; the viewport subscribes to it directly so scroll
2657
+ // re-renders only the viewport, not this whole component.
2658
+ useTranscriptScroll({
2659
+ totalLines: transcriptLines.length,
2660
+ viewportRows,
2661
+ active: !!props.fullscreen && !overlay && !taskBarFocused,
2662
+ resetToken: scrollResetToken,
2663
+ });
2664
+ const visibleQueuedCount = liveItems.filter((item) => item.kind === "queued").length;
2665
+ const hiddenQueuedCount = Math.max(0, agentLoop.queuedCount - visibleQueuedCount);
2666
+ const shouldTopSpaceQueueIndicator = hiddenQueuedCount > 0 &&
2667
+ shouldTopSpaceAfterPrintedAgentBoundary({
2668
+ currentKind: "queued",
2669
+ previousLiveItem: lastLiveItem,
2670
+ lastPendingHistoryItem,
2671
+ lastHistoryItem,
2672
+ });
2673
+ const handleRewindCancel = useCallback(() => setRewindCheckpoints(null), []);
2674
+ const handleRewindRestore = useCallback((id, mode) => {
2675
+ const store = props.checkpointStore;
2676
+ setRewindCheckpoints(null);
2677
+ if (!store)
2678
+ return;
2771
2679
  void (async () => {
2772
2680
  try {
2773
- await agentLoop.run(fullPrompt);
2681
+ const result = await store.restore(id, mode);
2682
+ const turnNum = id.replace(/^cp-0*/, "") || id;
2683
+ const detailParts = [];
2684
+ if (mode === "code" || mode === "both") {
2685
+ detailParts.push(`${result.filesRestored} file${result.filesRestored === 1 ? "" : "s"} restored`);
2686
+ }
2687
+ if (mode === "conversation" || mode === "both")
2688
+ detailParts.push("conversation rewound");
2689
+ const infoText = `Rewound to checkpoint #${turnNum} (${detailParts.join(", ")}).`;
2690
+ if (mode === "conversation" || mode === "both") {
2691
+ const truncated = messagesRef.current.slice(0, Math.max(1, result.messageIndex));
2692
+ messagesRef.current = truncated;
2693
+ persistedIndexRef.current = truncated.length;
2694
+ agentLoop.reset();
2695
+ // Remount in lockstep so the banner + confirmation re-render cleanly
2696
+ // after the conversation context is truncated (CLAUDE.md pattern).
2697
+ if (props.resetUI) {
2698
+ props.resetUI({
2699
+ wipeSession: true,
2700
+ messages: truncated,
2701
+ history: [
2702
+ { kind: "banner", id: "banner" },
2703
+ { kind: "info", text: infoText, id: getId() },
2704
+ ],
2705
+ });
2706
+ return;
2707
+ }
2708
+ }
2709
+ setLiveItems((prev) => [...prev, { kind: "info", text: infoText, id: getId() }]);
2774
2710
  }
2775
2711
  catch (err) {
2776
2712
  const msg = err instanceof Error ? err.message : String(err);
2777
- log("ERROR", "error", msg);
2778
- const isAbort = msg.includes("aborted") || msg.includes("abort");
2779
2713
  setLiveItems((prev) => [
2780
2714
  ...prev,
2781
- isAbort
2782
- ? { kind: "stopped", text: "Request was stopped.", id: getId() }
2783
- : toErrorItem(err, getId()),
2715
+ { kind: "info", text: `Rewind failed: ${msg}`, id: getId() },
2784
2716
  ]);
2785
- setRunAllTasks(false);
2786
2717
  }
2787
2718
  })();
2788
- }, [props.cwd, props.resetUI, props.sessionStore, agentLoop, currentProvider, currentModel]);
2789
- const openOverlay = useCallback((kind) => {
2719
+ }, [props.checkpointStore, props.resetUI, agentLoop, messagesRef, persistedIndexRef]);
2720
+ const handleCloseRemountableOverlay = () => {
2790
2721
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2791
- props.sessionStore.overlay = kind;
2792
- if (kind !== "plan")
2793
- props.sessionStore.planAutoExpand = false;
2722
+ props.sessionStore.overlay = null;
2794
2723
  props.resetUI();
2724
+ return;
2795
2725
  }
2796
- else {
2797
- if (props.sessionStore) {
2798
- props.sessionStore.overlay = kind;
2799
- if (kind !== "plan")
2800
- props.sessionStore.planAutoExpand = false;
2801
- if (agentLoop.isRunning && kind !== "goal" && kind !== "plan") {
2802
- props.sessionStore.pendingResetUI = true;
2803
- }
2804
- }
2805
- if (kind !== "plan")
2806
- setPlanAutoExpand(false);
2807
- setOverlay(kind);
2726
+ if (props.sessionStore) {
2727
+ props.sessionStore.overlay = null;
2728
+ if (agentLoop.isRunning)
2729
+ props.sessionStore.pendingResetUI = true;
2808
2730
  }
2809
- }, [agentLoop.isRunning, props]);
2810
- const closeOverlay = useCallback(() => {
2731
+ setOverlay(null);
2732
+ };
2733
+ const handleEnterPlanMode = useCallback(async (reason) => {
2734
+ await setPlanModeAndPrompt(true);
2735
+ setLiveItems((prev) => [
2736
+ ...prev,
2737
+ { kind: "plan_transition", text: reason ?? "", id: getId(), active: true },
2738
+ ]);
2739
+ }, [setPlanModeAndPrompt]);
2740
+ const handleExitPlanMode = useCallback(async (_planPath) => {
2741
+ await setPlanModeAndPrompt(false);
2742
+ planOverlayPendingRef.current = true;
2743
+ setPlanAutoExpand(true);
2744
+ if (props.sessionStore) {
2745
+ props.sessionStore.overlay = "plan";
2746
+ props.sessionStore.planAutoExpand = true;
2747
+ }
2748
+ setOverlay("plan");
2749
+ return "Plan submitted for user review. Wait for the user to approve, reject, or dismiss it before implementing.";
2750
+ }, [props.sessionStore, setPlanModeAndPrompt]);
2751
+ useEffect(() => {
2752
+ if (!props.planCallbacks)
2753
+ return;
2754
+ props.planCallbacks.onEnterPlan = handleEnterPlanMode;
2755
+ props.planCallbacks.onExitPlan = handleExitPlanMode;
2756
+ }, [handleEnterPlanMode, handleExitPlanMode, props.planCallbacks]);
2757
+ const handleCloseGoalOverlay = () => {
2811
2758
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2812
2759
  props.sessionStore.overlay = null;
2760
+ props.sessionStore.goalAutoExpand = false;
2813
2761
  props.resetUI();
2762
+ return;
2814
2763
  }
2815
- else {
2816
- if (props.sessionStore) {
2817
- props.sessionStore.overlay = null;
2818
- }
2819
- setOverlay(null);
2764
+ if (props.sessionStore) {
2765
+ props.sessionStore.overlay = null;
2766
+ props.sessionStore.goalAutoExpand = false;
2767
+ if (agentLoop.isRunning)
2768
+ props.sessionStore.pendingResetUI = true;
2820
2769
  }
2821
- }, [agentLoop.isRunning, overlay, props]);
2822
- const runGoalSyntheticEvent = useCallback((eventText) => {
2823
- const eventInfo = parseGoalSyntheticEvent(eventText);
2824
- const detail = eventInfo?.kind === "worker"
2825
- ? `Inspecting worker result${eventInfo.task ? ` for ${eventInfo.task}` : ""}.`
2826
- : `Inspecting verifier result${eventInfo?.status ? ` (${eventInfo.status})` : ""}.`;
2827
- if (agentRunningRef.current) {
2770
+ setGoalAutoExpand(false);
2771
+ setOverlay(null);
2772
+ };
2773
+ const handleRunGoal = (run) => {
2774
+ if (goalHasBlockingPrerequisites(run)) {
2775
+ upsertGoalStatusEntry({
2776
+ runId: run.id,
2777
+ label: run.title,
2778
+ phase: "failed",
2779
+ detail: formatGoalBlockingPrerequisites(run),
2780
+ startedAt: Date.now(),
2781
+ });
2828
2782
  appendGoalProgress({
2829
2783
  kind: "goal_progress",
2830
- phase: "orchestrator_reviewing",
2831
- title: "Goal update queued for orchestrator",
2832
- detail: `${detail} It will report back after the current turn.`,
2833
- workerId: eventInfo?.worker,
2834
- status: eventInfo?.status,
2784
+ phase: "terminal",
2785
+ title: `Goal blocked: ${run.title}`,
2786
+ detail: formatGoalBlockingPrerequisites(run),
2787
+ summaryRows: buildGoalSummaryRows(run),
2788
+ status: "blocked",
2835
2789
  });
2836
- agentLoop.queueMessage(eventText);
2837
2790
  return;
2838
2791
  }
2839
- appendGoalProgress({
2840
- kind: "goal_progress",
2841
- phase: "orchestrator_reviewing",
2842
- title: "Orchestrator reviewing Goal update",
2843
- detail,
2844
- workerId: eventInfo?.worker,
2845
- status: eventInfo?.status,
2846
- });
2847
- setLastUserMessage("");
2848
- setDoneStatus(null);
2849
- void agentLoop.run(eventText).catch((err) => {
2850
- log("ERROR", "goal", err instanceof Error ? err.message : String(err));
2851
- setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
2852
- });
2853
- }, [agentLoop, appendGoalProgress]);
2854
- const continueGoalRun = useCallback((runId) => {
2855
- void (async () => {
2856
- const latestRun = await reconcileActiveGoalRuns(props.cwd, {
2857
- isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
2858
- }).then(({ runs }) => runs.find((item) => item.id === runId) ?? null);
2859
- if (!latestRun) {
2860
- runningGoalIdsRef.current.delete(runId);
2861
- clearGoalStatusEntry(runId);
2862
- return;
2863
- }
2864
- const decision = decideGoalNextAction(latestRun);
2865
- if (decision.kind === "wait")
2866
- return;
2867
- if (decision.kind === "terminal" ||
2868
- decision.kind === "blocked" ||
2869
- decision.kind === "pause") {
2870
- const status = decision.kind === "terminal"
2871
- ? decision.status
2872
- : decision.kind === "blocked"
2873
- ? "blocked"
2874
- : "paused";
2875
- const nextRun = {
2876
- ...latestRun,
2877
- status,
2878
- continueRequestedAt: undefined,
2879
- blockers: decision.kind === "blocked" || decision.kind === "pause"
2880
- ? Array.from(new Set([...latestRun.blockers, decision.reason]))
2881
- : latestRun.blockers,
2882
- };
2883
- await upsertGoalRun(props.cwd, nextRun);
2884
- await appendGoalDecision(props.cwd, latestRun.id, {
2885
- kind: "continuation_stopped",
2886
- reason: decision.reason,
2887
- content: `terminal=${status}`,
2888
- });
2889
- const terminalProgress = formatGoalTerminalProgress(nextRun);
2890
- if (terminalProgress)
2891
- appendGoalProgress(terminalProgress);
2892
- runningGoalIdsRef.current.delete(runId);
2893
- clearGoalStatusEntry(runId);
2894
- return;
2895
- }
2896
- let runForNextAction = latestRun;
2897
- if (latestRun.continueRequestedAt &&
2898
- !listGoalWorkers(props.cwd).some((worker) => worker.status === "running") &&
2899
- activeVerifierRunIdsRef.current.size === 0) {
2900
- await appendGoalDecision(props.cwd, latestRun.id, {
2901
- kind: "continuation_consumed",
2902
- reason: `Continuation request consumed by ${decision.kind}.`,
2903
- });
2904
- runForNextAction = await upsertGoalRun(props.cwd, {
2905
- ...latestRun,
2906
- continueRequestedAt: undefined,
2907
- });
2908
- }
2909
- appendGoalProgress({
2910
- kind: "goal_progress",
2911
- phase: "continuing",
2912
- title: `Choosing next Goal step: ${latestRun.title}`,
2913
- detail: "Latest result is recorded; starting the next worker task or verifier automatically.",
2914
- status: latestRun.status,
2915
- });
2916
- upsertGoalStatusEntry({
2917
- runId: latestRun.id,
2918
- label: latestRun.title,
2919
- phase: "orchestrating",
2920
- startedAt: Date.now(),
2921
- detail: "choosing next step",
2922
- });
2923
- startGoalRunRef.current(runForNextAction);
2924
- })().catch((err) => {
2925
- runningGoalIdsRef.current.delete(runId);
2926
- clearGoalStatusEntry(runId);
2927
- log("ERROR", "goal", err instanceof Error ? err.message : String(err));
2928
- setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
2929
- });
2930
- }, [appendGoalProgress, clearGoalStatusEntry, props.cwd, upsertGoalStatusEntry]);
2931
- const handleGoalWorkerComplete = useCallback((run, completion) => {
2932
- const taskTitle = run.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
2933
- completion.worker.goalTaskId;
2934
- const eventText = formatGoalWorkerCompletionEvent(run, taskTitle, completion);
2935
- void summarizeGoalCounts(completion.worker.cwd).then((counts) => setGoalCount(counts.active));
2936
- appendGoalProgress({
2937
- kind: "goal_progress",
2938
- phase: "worker_finished",
2939
- title: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
2940
- detail: summarizeGoalCompletion(completion.summary),
2941
- workerId: completion.worker.id,
2942
- status: completion.status,
2943
- });
2944
- upsertGoalStatusEntry({
2945
- runId: run.id,
2946
- label: taskTitle,
2947
- phase: completion.status === "done" ? "reviewing" : "failed",
2948
- startedAt: Date.now(),
2949
- detail: completion.status === "done" ? "reviewing result" : "task failed",
2950
- workerId: completion.worker.id,
2951
- goalNumber: goalNumberForRun(run.id),
2952
- });
2953
- runGoalSyntheticEvent(eventText);
2954
2792
  void (async () => {
2955
- if (listGoalWorkers(completion.worker.cwd).some((worker) => worker.status === "running"))
2956
- return;
2957
- if (activeVerifierRunIdsRef.current.size > 0)
2958
- return;
2959
- const runs = await loadGoalRuns(completion.worker.cwd);
2960
- const queued = runs.find((item) => item.continueRequestedAt && !goalHasBlockingPrerequisites(item));
2961
- if (queued)
2962
- setTimeout(() => continueGoalRun(queued.id), 750);
2963
- })().catch((err) => log("ERROR", "goal", err instanceof Error ? err.message : String(err)));
2964
- }, [
2965
- appendGoalProgress,
2966
- continueGoalRun,
2967
- goalNumberForRun,
2968
- runGoalSyntheticEvent,
2969
- upsertGoalStatusEntry,
2970
- ]);
2971
- useEffect(() => {
2972
- return subscribeGoalWorkerCompletions((completion) => {
2973
- void (async () => {
2974
- const latestRun = (await loadGoalRuns(completion.worker.cwd)).find((item) => item.id === completion.worker.goalRunId) ?? null;
2975
- if (!latestRun) {
2976
- log("WARN", "goal", `Worker completion for unknown Goal ${completion.worker.goalRunId}`);
2977
- return;
2978
- }
2979
- runningGoalIdsRef.current.add(latestRun.id);
2980
- handleGoalWorkerComplete(latestRun, completion);
2981
- })().catch((err) => {
2982
- log("ERROR", "goal", err instanceof Error ? err.message : String(err));
2983
- setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
2984
- });
2985
- }, props.cwd);
2986
- }, [handleGoalWorkerComplete, props.cwd]);
2987
- const startGoalRun = useCallback((run) => {
2988
- runningGoalIdsRef.current.add(run.id);
2989
- void (async () => {
2990
- if (goalHasBlockingPrerequisites(run)) {
2991
- setOverlay(null);
2992
- const detail = formatGoalBlockingPrerequisites(run);
2993
- await upsertGoalRun(props.cwd, {
2994
- ...run,
2995
- status: "blocked",
2996
- blockers: Array.from(new Set([...run.blockers, detail])),
2997
- });
2998
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
2999
- appendGoalProgress({
3000
- kind: "goal_progress",
3001
- phase: "terminal",
3002
- title: `Goal blocked: ${run.title}`,
3003
- detail,
3004
- status: "blocked",
3005
- });
3006
- runningGoalIdsRef.current.delete(run.id);
3007
- clearGoalStatusEntry(run.id);
3008
- return;
3009
- }
3010
- const decision = decideGoalNextAction(run);
3011
- await appendGoalDecision(props.cwd, run.id, decision);
3012
- if (decision.kind === "terminal") {
3013
- const terminalProgress = formatGoalTerminalProgress(run);
3014
- if (terminalProgress)
3015
- appendGoalProgress(terminalProgress);
3016
- runningGoalIdsRef.current.delete(run.id);
3017
- clearGoalStatusEntry(run.id);
3018
- return;
3019
- }
3020
- if (decision.kind === "wait") {
3021
- appendGoalProgress({
3022
- kind: "goal_progress",
3023
- phase: "worker_started",
3024
- title: decision.workerId ? `Goal working: ${run.title}` : `Goal active: ${run.title}`,
3025
- detail: decision.reason,
3026
- workerId: decision.workerId,
3027
- });
2793
+ const checked = await runGoalPrerequisiteChecks(props.cwd, run);
2794
+ const current = await upsertGoalRun(props.cwd, checked.run);
2795
+ if (goalHasBlockingPrerequisites(current)) {
3028
2796
  upsertGoalStatusEntry({
3029
- runId: run.id,
3030
- label: run.title,
3031
- phase: decision.workerId ? "worker" : "orchestrating",
2797
+ runId: current.id,
2798
+ label: current.title,
2799
+ phase: "failed",
2800
+ detail: formatGoalBlockingPrerequisites(current),
3032
2801
  startedAt: Date.now(),
3033
- detail: decision.reason,
3034
- workerId: decision.workerId,
3035
- goalNumber: goalNumberForRun(run.id),
3036
2802
  });
3037
- return;
3038
- }
3039
- if (decision.kind === "complete") {
3040
- await upsertGoalRun(props.cwd, { ...run, status: "passed" });
3041
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3042
2803
  appendGoalProgress({
3043
2804
  kind: "goal_progress",
3044
2805
  phase: "terminal",
3045
- title: `Goal passed: ${run.title}`,
3046
- detail: decision.reason,
3047
- status: "passed",
3048
- });
3049
- runningGoalIdsRef.current.delete(run.id);
3050
- clearGoalStatusEntry(run.id);
3051
- return;
3052
- }
3053
- if (decision.kind === "run_verifier") {
3054
- await verifyGoalRun(run);
3055
- return;
3056
- }
3057
- if (decision.kind === "create_task") {
3058
- await updateGoalTask(props.cwd, run.id, `auto-${Date.now()}`, {
3059
- title: decision.title,
3060
- prompt: decision.prompt,
3061
- status: "pending",
3062
- });
3063
- const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
3064
- await upsertGoalRun(props.cwd, { ...latestRun, status: "ready" });
3065
- setTimeout(() => continueGoalRun(run.id), 250);
3066
- return;
3067
- }
3068
- if (decision.kind === "blocked") {
3069
- await upsertGoalRun(props.cwd, {
3070
- ...run,
3071
- status: "blocked",
3072
- blockers: [...run.blockers, decision.reason],
3073
- });
3074
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3075
- appendGoalProgress({
3076
- kind: "goal_progress",
3077
- phase: "terminal",
3078
- title: `Goal blocked: ${run.title}`,
3079
- detail: decision.reason,
3080
- status: "blocked",
3081
- });
3082
- runningGoalIdsRef.current.delete(run.id);
3083
- clearGoalStatusEntry(run.id);
3084
- return;
3085
- }
3086
- if (decision.kind === "pause") {
3087
- await updateGoalTask(props.cwd, run.id, decision.task.id, {
2806
+ title: `Goal blocked: ${current.title}`,
2807
+ detail: formatGoalBlockingPrerequisites(current),
2808
+ summaryRows: buildGoalSummaryRows(current),
3088
2809
  status: "blocked",
3089
- attempts: decision.attempts,
3090
- lastSummary: "Paused after worker attempt limit.",
3091
2810
  });
3092
- await upsertGoalRun(props.cwd, {
3093
- ...run,
3094
- status: "paused",
3095
- blockers: [...run.blockers, decision.reason],
3096
- });
3097
- await appendGoalEvidence(props.cwd, run.id, {
3098
- kind: "summary",
3099
- label: "Goal paused",
3100
- content: decision.reason,
3101
- });
3102
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3103
- appendGoalProgress({
3104
- kind: "goal_progress",
3105
- phase: "terminal",
3106
- title: `Goal paused: ${run.title}`,
3107
- detail: decision.reason,
3108
- status: "paused",
3109
- });
3110
- runningGoalIdsRef.current.delete(run.id);
3111
- clearGoalStatusEntry(run.id);
3112
2811
  return;
3113
2812
  }
3114
- await updateGoalTask(props.cwd, run.id, decision.task.id, { attempts: decision.attempts });
3115
- const worker = await startGoalWorker({
3116
- cwd: props.cwd,
3117
- provider: currentProvider,
3118
- model: currentModel,
3119
- goalRunId: run.id,
3120
- goalTaskId: decision.task.id,
3121
- taskTitle: decision.task.title,
3122
- prompt: decision.task.prompt,
2813
+ const resumed = await upsertGoalRun(props.cwd, {
2814
+ ...current,
2815
+ status: current.status === "draft" ? "ready" : current.status,
2816
+ continueRequestedAt: new Date().toISOString(),
3123
2817
  });
3124
- await upsertGoalRun(props.cwd, {
3125
- ...run,
3126
- status: "running",
3127
- activeWorkerId: worker.id,
3128
- continueRequestedAt: undefined,
2818
+ setGoalAutoExpand(true);
2819
+ upsertGoalStatusEntry({
2820
+ runId: resumed.id,
2821
+ label: resumed.title,
2822
+ phase: "orchestrating",
2823
+ detail: "Choosing next Goal action",
2824
+ startedAt: Date.now(),
3129
2825
  });
3130
- setOverlay(null);
3131
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3132
2826
  appendGoalProgress({
3133
2827
  kind: "goal_progress",
3134
- phase: "worker_started",
3135
- title: `Worker started: ${decision.task.title}`,
3136
- detail: "Task is running in the background.",
3137
- workerId: worker.id,
3138
- status: worker.status,
3139
- });
3140
- upsertGoalStatusEntry({
3141
- runId: run.id,
3142
- label: decision.task.title,
3143
- phase: "worker",
3144
- startedAt: Date.now(),
3145
- detail: "background worker running",
3146
- workerId: worker.id,
3147
- goalNumber: goalNumberForRun(run.id),
2828
+ phase: "orchestrator_reviewing",
2829
+ title: `Goal ${goalNumberForRun(resumed.id)} continuing`,
2830
+ detail: resumed.title,
2831
+ status: resumed.status,
3148
2832
  });
3149
- })().catch((err) => {
3150
- clearGoalStatusEntry(run.id);
3151
- log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3152
- setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
3153
- });
3154
- }, [
3155
- props.cwd,
3156
- currentProvider,
3157
- currentModel,
3158
- appendGoalProgress,
3159
- clearGoalStatusEntry,
3160
- goalNumberForRun,
3161
- upsertGoalStatusEntry,
3162
- ]);
3163
- const verifyGoalRun = useCallback(async (run) => {
2833
+ continueGoalRun(resumed);
2834
+ })();
2835
+ };
2836
+ startGoalRunRef.current = handleRunGoal;
2837
+ const handleVerifyGoal = (run) => {
3164
2838
  if (!run.verifier?.command) {
3165
- await appendGoalEvidence(props.cwd, run.id, {
3166
- kind: "summary",
3167
- label: "Missing verifier",
3168
- content: "No verifier command is configured.",
3169
- });
3170
- await upsertGoalRun(props.cwd, {
3171
- ...run,
3172
- status: "blocked",
3173
- blockers: [...run.blockers, "No verifier command configured."],
3174
- });
3175
2839
  appendGoalProgress({
3176
2840
  kind: "goal_progress",
3177
2841
  phase: "terminal",
3178
- title: `Goal blocked: ${run.title}`,
3179
- detail: "No verifier command is configured.",
2842
+ title: `Goal verifier missing: ${run.title}`,
2843
+ detail: "Define a verifier command before running verification.",
3180
2844
  status: "blocked",
3181
2845
  });
3182
- runningGoalIdsRef.current.delete(run.id);
3183
- clearGoalStatusEntry(run.id);
3184
2846
  return;
3185
2847
  }
3186
- activeVerifierRunIdsRef.current.add(run.id);
3187
- await upsertGoalRun(props.cwd, {
3188
- ...run,
3189
- status: "verifying",
3190
- continueRequestedAt: undefined,
3191
- });
3192
- appendGoalProgress({
3193
- kind: "goal_progress",
3194
- phase: "verifier_started",
3195
- title: `Verifier started: ${run.title}`,
3196
- detail: run.verifier.command,
3197
- status: "verifying",
3198
- });
3199
- const startedAt = Date.now();
3200
- const verifierTimeoutMs = Number(process.env.GG_GOAL_VERIFIER_TIMEOUT_MS ?? 10 * 60 * 1000);
3201
- upsertGoalStatusEntry({
3202
- runId: run.id,
3203
- label: run.title,
3204
- phase: "verifier",
3205
- startedAt,
3206
- detail: run.verifier.command,
3207
- goalNumber: goalNumberForRun(run.id),
3208
- });
3209
- void runGoalVerifierCommand({
3210
- cwd: props.cwd,
3211
- runId: run.id,
3212
- command: run.verifier.command,
3213
- timeoutMs: verifierTimeoutMs,
3214
- now: () => startedAt,
3215
- })
3216
- .then(async ({ verification, failureClass, durationMs }) => {
3217
- activeVerifierRunIdsRef.current.delete(run.id);
3218
- const status = verification.status;
3219
- const summary = verification.summary;
3220
- const outputPath = verification.outputPath;
3221
- const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
3222
- const runWithVerifier = {
3223
- ...latestRun,
2848
+ void (async () => {
2849
+ const verifying = await upsertGoalRun(props.cwd, {
2850
+ ...run,
2851
+ status: "ready",
2852
+ tasks: run.tasks.map((task) => task.status === "pending" || task.status === "failed"
2853
+ ? { ...task, status: "done" }
2854
+ : task),
3224
2855
  verifier: {
3225
- ...latestRun.verifier,
3226
- description: latestRun.verifier?.description ?? "Goal verifier",
3227
- command: run.verifier?.command,
3228
- lastResult: verification,
2856
+ ...run.verifier,
2857
+ lastResult: undefined,
3229
2858
  },
3230
- };
3231
- const completionCheck = canCompleteGoalRun(runWithVerifier);
3232
- const verifiedRun = await upsertGoalRun(props.cwd, {
3233
- ...runWithVerifier,
3234
- continueRequestedAt: undefined,
3235
- status: status === "pass" && completionCheck.ok
3236
- ? "passed"
3237
- : status === "pass"
3238
- ? "ready"
3239
- : "failed",
3240
- });
3241
- await appendGoalEvidence(props.cwd, run.id, {
3242
- kind: "command",
3243
- label: `Verifier ${status}`,
3244
- content: `${failureClass}: ${summary}`.slice(0, 4000),
3245
- path: outputPath,
3246
2859
  });
3247
- await appendGoalDecision(props.cwd, run.id, {
3248
- kind: `verifier_${status}`,
3249
- reason: `${failureClass}: verifier exited with code ${verification.exitCode ?? 1}.`,
3250
- content: `outputPath=${outputPath ?? ""}; durationMs=${durationMs}`,
3251
- });
3252
- if (status === "fail" && shouldCreateVerifierFixTask(latestRun)) {
3253
- await updateGoalTask(props.cwd, run.id, `fix-${Date.now()}`, {
3254
- title: "Fix verifier failure",
3255
- prompt: `Goal verifier failed after ${durationMs}ms. Original goal: ${run.goal}\n\n` +
3256
- `Verifier command: ${run.verifier?.command}\n\n` +
3257
- `Failure output:\n${summary.slice(-6000)}\n\nFix the cause, record evidence with the goals tool, and rerun relevant verification.`,
3258
- status: "pending",
3259
- });
3260
- const runWithPendingFix = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? latestRun;
3261
- await upsertGoalRun(props.cwd, { ...runWithPendingFix, status: "ready" });
2860
+ continueGoalRun(verifying);
2861
+ })();
2862
+ };
2863
+ const handlePauseGoal = (run) => {
2864
+ void (async () => {
2865
+ for (const worker of listGoalWorkers(props.cwd).filter((item) => item.goalRunId === run.id)) {
2866
+ await stopGoalWorker(worker.id);
3262
2867
  }
3263
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3264
- appendGoalProgress({
3265
- kind: "goal_progress",
3266
- phase: "verifier_finished",
3267
- title: `Verifier ${status}: ${run.title}`,
3268
- detail: summarizeGoalCompletion(summary),
3269
- status,
3270
- });
3271
- upsertGoalStatusEntry({
3272
- runId: run.id,
3273
- label: run.title,
3274
- phase: status === "pass" ? "reviewing" : "failed",
3275
- startedAt: Date.now(),
3276
- detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
3277
- goalNumber: goalNumberForRun(run.id),
2868
+ const paused = await upsertGoalRun(props.cwd, {
2869
+ ...run,
2870
+ status: "paused",
2871
+ blockers: [...run.blockers, "Paused by user from Goal pane."],
3278
2872
  });
3279
- const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status === "pass" ? "pass" : "fail", run.verifier?.command ?? "", verification.exitCode ?? 1, summary);
3280
- runGoalSyntheticEvent(eventText);
3281
- const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
3282
- if (continuationRun?.continueRequestedAt && status === "pass") {
3283
- setTimeout(() => continueGoalRun(run.id), 500);
3284
- }
3285
- })
3286
- .catch((err) => {
3287
- activeVerifierRunIdsRef.current.delete(run.id);
3288
2873
  clearGoalStatusEntry(run.id);
3289
- log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3290
- setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
3291
- });
3292
- }, [
3293
- props.cwd,
3294
- appendGoalProgress,
3295
- clearGoalStatusEntry,
3296
- goalNumberForRun,
3297
- runGoalSyntheticEvent,
3298
- upsertGoalStatusEntry,
3299
- ]);
3300
- const pauseGoalRun = useCallback((run) => {
3301
- void (async () => {
3302
- runningGoalIdsRef.current.delete(run.id);
3303
- if (run.activeWorkerId)
3304
- await stopGoalWorker(run.activeWorkerId);
3305
- await upsertGoalRun(props.cwd, { ...run, status: "paused", activeWorkerId: undefined });
3306
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3307
2874
  appendGoalProgress({
3308
2875
  kind: "goal_progress",
3309
2876
  phase: "terminal",
3310
- title: `Goal paused: ${run.title}`,
3311
- detail: "Auto-continuation stopped until resumed.",
2877
+ title: `Goal paused: ${paused.title}`,
2878
+ detail: "Paused by user from Goal pane.",
2879
+ summaryRows: buildGoalSummaryRows(paused),
3312
2880
  status: "paused",
3313
2881
  });
3314
- clearGoalStatusEntry(run.id);
3315
- })().catch((err) => {
3316
- log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3317
- setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
3318
- });
3319
- }, [appendGoalProgress, clearGoalStatusEntry, props.cwd]);
3320
- // Keep refs in sync for access from stale closures (onDone)
3321
- startTaskRef.current = startTask;
3322
- startGoalRunRef.current = startGoalRun;
3323
- useEffect(() => {
3324
- runAllTasksRef.current = runAllTasks;
2882
+ })();
2883
+ };
2884
+ const handleRefineGoal = (run, feedback) => {
2885
+ setOverlay(null);
2886
+ setGoalAutoExpand(false);
2887
+ setDoneStatus(null);
2888
+ appendGoalAgentTransition(`Goal refinement requested for ${run.title}`);
2889
+ void agentLoop
2890
+ .run(`Refine Goal run ${run.id} (${run.title}) using this user feedback:\n\n${feedback}\n\nUpdate the durable Goal plan with the goals tool. Do not start workers until the user approves/runs the Goal.`)
2891
+ .catch((err) => setLiveItems((prev) => [...prev, toErrorItem(err, getId())]));
2892
+ };
2893
+ const handleClosePlanOverlay = () => {
2894
+ planOverlayPendingRef.current = false;
2895
+ if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2896
+ props.sessionStore.overlay = null;
2897
+ props.sessionStore.planAutoExpand = false;
2898
+ props.resetUI();
2899
+ return;
2900
+ }
3325
2901
  if (props.sessionStore) {
3326
- props.sessionStore.runAllTasks = runAllTasks;
3327
- // Keep currentTaskId in sync with the ref so a sessionStore-seeded
3328
- // remount picks up the same value the imperative sites wrote.
3329
- props.sessionStore.currentTaskId = currentTaskIdRef.current;
2902
+ props.sessionStore.overlay = null;
2903
+ props.sessionStore.planAutoExpand = false;
2904
+ if (agentLoop.isRunning)
2905
+ props.sessionStore.pendingResetUI = true;
3330
2906
  }
3331
- }, [runAllTasks, props.sessionStore]);
3332
- useEffect(() => {
3333
- agentRunningRef.current = agentLoop.isRunning;
3334
- }, [agentLoop.isRunning]);
3335
- const startPixelFix = useCallback((errorId) => {
2907
+ setPlanAutoExpand(false);
2908
+ setOverlay(null);
2909
+ };
2910
+ const handlePixelFixOne = (entry) => {
2911
+ setOverlay(null);
2912
+ startPixelFix(entry.errorId);
2913
+ };
2914
+ const handlePixelFixAll = (entries) => {
2915
+ const first = entries.find((entry) => entry.status === "open") ?? entries[0];
2916
+ if (!first)
2917
+ return;
2918
+ setOverlay(null);
2919
+ setRunAllPixel(true);
2920
+ startPixelFix(first.errorId);
2921
+ };
2922
+ const handleApprovePlan = (planPath) => {
2923
+ log("INFO", "plan", "Plan approved — transitioning to implementation", {
2924
+ planPath,
2925
+ });
2926
+ planOverlayPendingRef.current = false;
3336
2927
  void (async () => {
3337
2928
  try {
3338
- const { preparePixelFix } = await import("../core/pixel-fix.js");
3339
- const prep = await preparePixelFix(errorId);
3340
- currentPixelFixRef.current = prep;
3341
- // Move the agent into the error's project root. Four things must
3342
- // change in lockstep, otherwise the agent (or the chrome around
3343
- // it) shows the wrong project:
3344
- // 1. process.cwd — for any code reading it directly
3345
- // 2. cwd-bound tools (read/write/bash/grep/…) — baked at creation
3346
- // 3. the system prompt's "Working directory: …" line the only
3347
- // place the model itself learns where it is
3348
- // 4. displayedCwd state — Banner + Footer read this for display
3349
- try {
3350
- process.chdir(prep.projectPath);
3351
- }
3352
- catch (err) {
3353
- log("WARN", "pixel", `chdir failed: ${err.message}`);
2929
+ // Read plan steps for progress tracking — handed to the new
2930
+ // mount via sessionStore.planSteps below.
2931
+ const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
2932
+ const steps = extractPlanSteps(planContent);
2933
+ // Build the new system prompt with the approved plan baked in.
2934
+ const newPrompt = await rebuildSystemPrompt({
2935
+ approvedPlanPath: planPath,
2936
+ });
2937
+ // Create a new session file BEFORE remount so the new tree
2938
+ // picks it up via sessionStore.sessionPath.
2939
+ let newSessionPath;
2940
+ const sm = sessionManagerRef.current;
2941
+ if (sm) {
2942
+ const s = await sm.create(props.cwd, currentProvider, currentModel);
2943
+ newSessionPath = s.path;
3354
2944
  }
3355
- cwdRef.current = prep.projectPath;
3356
- repoMapDirtyRef.current = true;
3357
- repoMapMarkdownRef.current = "";
3358
- repoMapSnapshotRef.current = undefined;
3359
- repoMapChangedCountRef.current = 0;
3360
- repoMapCacheRef.current = createRepoMapCache();
3361
- props.repoMapChangedFilesRef?.current.clear();
3362
- props.repoMapReadFilesRef?.current.clear();
3363
- setDisplayedCwd(prep.projectPath);
3364
- let toolsForPixelFix = currentToolsRef.current;
3365
- if (props.rebuildToolsForCwd) {
3366
- toolsForPixelFix = props.rebuildToolsForCwd(prep.projectPath);
3367
- currentToolsRef.current = toolsForPixelFix;
3368
- setCurrentTools(toolsForPixelFix);
2945
+ if (props.resetUI && props.sessionStore) {
2946
+ // Clear the overlay so the new mount lands on the chat,
2947
+ // not back inside the plan pane.
2948
+ props.sessionStore.overlay = null;
2949
+ props.sessionStore.planAutoExpand = false;
2950
+ props.resetUI({
2951
+ wipeSession: true,
2952
+ messages: [{ role: "system", content: newPrompt }],
2953
+ approvedPlanPath: planPath,
2954
+ planSteps: steps,
2955
+ sessionPath: newSessionPath,
2956
+ pendingAction: {
2957
+ prompt: "The plan has been approved. Implement it now, following each step in order.",
2958
+ planEvent: { event: "approved" },
2959
+ },
2960
+ });
2961
+ return;
3369
2962
  }
3370
- // Pixel-fix swaps the project rootreset injected packs so the
3371
- // new project re-detects from scratch on the next tool call. Also
3372
- // reset the setup-hint flag so the new project's first badge re-
3373
- // surfaces the tip (different project, may need the reminder).
3374
- injectedLanguagesRef.current = new Set();
3375
- setupHintShownRef.current = false;
3376
- const detectedForPixelFix = detectLanguages(prep.projectPath);
3377
- injectedLanguagesRef.current = detectedForPixelFix;
3378
- const newSystemPrompt = await rebuildSystemPrompt({
3379
- cwd: prep.projectPath,
3380
- planMode: false,
3381
- clearApprovedPlan: true,
3382
- activeLanguages: detectedForPixelFix,
3383
- tools: toolsForPixelFix,
3384
- });
3385
- // Now that the cwd swap is committed, reset chat. Do not clear the
3386
- // terminal here; terminal clear sequences can erase saved scrollback.
2963
+ // Fallback path (resetUI not wiredtests). Mutate in place.
2964
+ approvedPlanPathRef.current = planPath;
2965
+ planStepsRef.current = steps;
2966
+ setPlanSteps(steps);
2967
+ clearPendingHistory();
3387
2968
  setHistory([{ kind: "banner", id: "banner" }]);
3388
2969
  setLiveItems([]);
3389
- messagesRef.current = messagesRef.current.slice(0, 1);
2970
+ setPlanAutoExpand(false);
2971
+ setOverlay(null);
2972
+ messagesRef.current = [{ role: "system", content: newPrompt }];
3390
2973
  agentLoop.reset();
3391
2974
  persistedIndexRef.current = messagesRef.current.length;
3392
- const sm = sessionManagerRef.current;
3393
- if (sm) {
3394
- void sm.create(prep.projectPath, currentProvider, currentModel).then((s) => {
3395
- sessionPathRef.current = s.path;
3396
- log("INFO", "pixel", "New session for pixel fix", { path: s.path });
3397
- });
3398
- }
3399
- if (messagesRef.current[0]?.role === "system") {
3400
- messagesRef.current[0] = { role: "system", content: newSystemPrompt };
3401
- }
3402
- else {
3403
- messagesRef.current.unshift({ role: "system", content: newSystemPrompt });
3404
- }
3405
- const title = `Fix ${errorId.slice(0, 12)}… in ${prep.projectName}`;
3406
- const taskItem = { kind: "task", title, id: getId() };
3407
- setLastUserMessage(title);
2975
+ if (newSessionPath)
2976
+ sessionPathRef.current = newSessionPath;
2977
+ setLiveItems([
2978
+ {
2979
+ kind: "info",
2980
+ text: "Plan approved — starting fresh session for implementation",
2981
+ id: getId(),
2982
+ },
2983
+ ]);
3408
2984
  setDoneStatus(null);
3409
- setLiveItems([taskItem]);
3410
- await agentLoop.run(prep.prompt);
2985
+ await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
3411
2986
  }
3412
2987
  catch (err) {
3413
- const msg = err instanceof Error ? err.message : String(err);
3414
- log("ERROR", "pixel", msg);
3415
- currentPixelFixRef.current = null;
3416
- setRunAllPixel(false);
2988
+ const errMsg = err instanceof Error ? err.message : String(err);
2989
+ log("ERROR", "error", errMsg);
3417
2990
  setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3418
2991
  }
3419
2992
  })();
3420
- }, [props.cwd, agentLoop, currentProvider, currentModel]);
3421
- startPixelFixRef.current = startPixelFix;
3422
- // Seed from sessionStore so "Fix All" chaining survives a deferred
3423
- // resetUI() if it fires between pixel fixes (e.g. user toggled a pane).
3424
- const [runAllPixel, setRunAllPixel] = useState(props.sessionStore?.runAllPixel ?? false);
3425
- useEffect(() => {
3426
- runAllPixelRef.current = runAllPixel;
3427
- if (props.sessionStore)
3428
- props.sessionStore.runAllPixel = runAllPixel;
3429
- }, [runAllPixel, props.sessionStore]);
3430
- const isTaskView = overlay === "tasks";
3431
- const isGoalView = overlay === "goal";
3432
- const isSkillsView = overlay === "skills";
3433
- const isPlanView = overlay === "plan";
3434
- const isEyesView = overlay === "eyes";
3435
- const footerStatusLayout = getFooterStatusLayoutDecision({
3436
- columns,
3437
- backgroundTaskCount: bgTasks.length,
3438
- eyesCount,
3439
- updatePending,
3440
- });
3441
- const isPixelView = overlay === "pixel";
3442
- const isOverlayView = isTaskView || isGoalView || isSkillsView || isPlanView || isEyesView || isPixelView;
3443
- const shouldHideHistoryForOverlay = shouldHideHistoryForOverlayView(isOverlayView, agentLoop.isRunning);
3444
- const stabilizeOverlayPaneRerender = shouldStabilizeOverlayPaneRerender({
3445
- overlayPane: overlay,
3446
- isAgentRunning: agentLoop.isRunning,
3447
- });
3448
- const staticItems = shouldHideStaticItemsForOverlayView({
3449
- shouldHideHistoryForOverlay,
3450
- stabilizeOverlayPaneRerender,
3451
- })
3452
- ? []
3453
- : history;
3454
- return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: staticItems, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, getStaticHistoryKey({ resizeKey })), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
3455
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3456
- props.sessionStore.overlay = null;
3457
- props.resetUI();
3458
- }
3459
- else {
3460
- if (props.sessionStore) {
3461
- props.sessionStore.overlay = null;
3462
- if (agentLoop.isRunning)
3463
- props.sessionStore.pendingResetUI = true;
3464
- }
3465
- setTaskCount(getTaskCount(props.cwd));
3466
- setOverlay(null);
3467
- }
3468
- }, onWorkOnTask: (title, prompt, taskId) => {
3469
- setOverlay(null);
3470
- startTask(title, prompt, taskId);
3471
- }, onRunAllTasks: () => {
3472
- setOverlay(null);
3473
- setRunAllTasks(true);
3474
- const next = getNextPendingTask(props.cwd);
3475
- if (next) {
3476
- markTaskInProgress(props.cwd, next.id);
3477
- startTask(next.title, next.prompt, next.id);
3478
- }
3479
- } })) : isGoalView ? (_jsx(GoalOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
3480
- void summarizeGoalCounts(props.cwd).then((counts) => setGoalCount(counts.active));
3481
- closeOverlay();
3482
- }, onRunGoal: (run) => {
3483
- setOverlay(null);
3484
- startGoalRun(run);
3485
- }, onVerifyGoal: (run) => {
3486
- void verifyGoalRun(run);
3487
- }, onPauseGoal: (run) => {
3488
- pauseGoalRun(run);
3489
- } })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
3490
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3491
- props.sessionStore.overlay = null;
3492
- props.resetUI();
3493
- }
3494
- else {
3495
- if (props.sessionStore) {
3496
- props.sessionStore.overlay = null;
3497
- if (agentLoop.isRunning)
3498
- props.sessionStore.pendingResetUI = true;
3499
- }
3500
- setOverlay(null);
3501
- }
3502
- }, onFixOne: (entry) => {
3503
- setOverlay(null);
3504
- startPixelFix(entry.errorId);
3505
- }, onFixAll: (entries) => {
3506
- const first = entries.find((e) => e.status === "open") ?? entries[0];
3507
- if (!first)
3508
- return;
3509
- setOverlay(null);
3510
- setRunAllPixel(true);
3511
- startPixelFix(first.errorId);
3512
- } })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
3513
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3514
- props.sessionStore.overlay = null;
3515
- props.resetUI();
3516
- }
3517
- else {
3518
- if (props.sessionStore) {
3519
- props.sessionStore.overlay = null;
3520
- if (agentLoop.isRunning)
3521
- props.sessionStore.pendingResetUI = true;
3522
- }
3523
- setOverlay(null);
3524
- }
3525
- } })) : isEyesView ? (_jsx(EyesOverlay, { cwd: props.cwd, onClose: () => {
3526
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3527
- props.sessionStore.overlay = null;
3528
- props.resetUI();
3529
- }
3530
- else {
3531
- if (props.sessionStore) {
3532
- props.sessionStore.overlay = null;
3533
- if (agentLoop.isRunning)
3534
- props.sessionStore.pendingResetUI = true;
3535
- }
3536
- setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
3537
- setOverlay(null);
3538
- }
3539
- }, onQueueMessage: (msg) => {
3540
- agentLoop.queueMessage(msg);
3541
- } })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
3542
- planOverlayPendingRef.current = false;
3543
- if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3544
- props.sessionStore.overlay = null;
3545
- props.sessionStore.planAutoExpand = false;
3546
- props.resetUI();
3547
- }
3548
- else {
3549
- if (props.sessionStore) {
3550
- props.sessionStore.overlay = null;
3551
- props.sessionStore.planAutoExpand = false;
3552
- if (agentLoop.isRunning)
3553
- props.sessionStore.pendingResetUI = true;
3554
- }
3555
- setPlanAutoExpand(false);
3556
- setOverlay(null);
3557
- }
3558
- }, onApprove: (planPath) => {
3559
- log("INFO", "plan", "Plan approved — transitioning to implementation", {
3560
- planPath,
3561
- });
3562
- planOverlayPendingRef.current = false;
3563
- void (async () => {
3564
- try {
3565
- // Read plan steps for progress tracking — handed to the new
3566
- // mount via sessionStore.planSteps below.
3567
- const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
3568
- const steps = extractPlanSteps(planContent);
3569
- // Build the new system prompt with the approved plan baked in.
3570
- const newPrompt = await rebuildSystemPrompt({
3571
- planMode: false,
3572
- approvedPlanPath: planPath,
3573
- });
3574
- // Create a new session file BEFORE remount so the new tree
3575
- // picks it up via sessionStore.sessionPath.
3576
- let newSessionPath;
3577
- const sm = sessionManagerRef.current;
3578
- if (sm) {
3579
- const s = await sm.create(props.cwd, currentProvider, currentModel);
3580
- newSessionPath = s.path;
3581
- }
3582
- if (props.resetUI && props.sessionStore) {
3583
- // Clear the overlay so the new mount lands on the chat,
3584
- // not back inside the plan pane.
3585
- props.sessionStore.overlay = null;
3586
- props.sessionStore.planAutoExpand = false;
3587
- props.resetUI({
3588
- wipeSession: true,
3589
- messages: [{ role: "system", content: newPrompt }],
3590
- approvedPlanPath: planPath,
3591
- planSteps: steps,
3592
- sessionPath: newSessionPath,
3593
- pendingAction: {
3594
- prompt: "The plan has been approved. Implement it now, following each step in order.",
3595
- planEvent: { event: "approved" },
3596
- },
3597
- });
3598
- return;
3599
- }
3600
- // Fallback path (resetUI not wired — tests). Mutate in place.
3601
- approvedPlanPathRef.current = planPath;
3602
- planStepsRef.current = steps;
3603
- setPlanSteps(steps);
3604
- setHistory([{ kind: "banner", id: "banner" }]);
3605
- setLiveItems([]);
3606
- setPlanAutoExpand(false);
3607
- setOverlay(null);
3608
- messagesRef.current = [{ role: "system", content: newPrompt }];
3609
- agentLoop.reset();
3610
- persistedIndexRef.current = messagesRef.current.length;
3611
- if (newSessionPath)
3612
- sessionPathRef.current = newSessionPath;
3613
- setLiveItems([
3614
- {
3615
- kind: "info",
3616
- text: "Plan approved — starting fresh session for implementation",
3617
- id: getId(),
3618
- },
3619
- ]);
3620
- setDoneStatus(null);
3621
- await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
3622
- }
3623
- catch (err) {
3624
- const errMsg = err instanceof Error ? err.message : String(err);
3625
- log("ERROR", "error", errMsg);
3626
- setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3627
- }
3628
- })();
3629
- }, onReject: (planPath, feedback) => {
3630
- planOverlayPendingRef.current = false;
3631
- const rejectionMsg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
3632
- `Please revise the plan based on this feedback.`;
3633
- if (props.resetUI && props.sessionStore) {
3634
- props.sessionStore.overlay = null;
3635
- props.sessionStore.planAutoExpand = false;
3636
- // No wipeSession — keep history, messages, plan mode etc. The
3637
- // agent picks up the rejection mid-conversation.
3638
- props.resetUI({
3639
- pendingAction: {
3640
- prompt: rejectionMsg,
3641
- planEvent: { event: "rejected", detail: feedback },
3642
- },
3643
- });
3644
- return;
3645
- }
3646
- setPlanAutoExpand(false);
3647
- setOverlay(null);
3648
- setDoneStatus(null);
3649
- setLiveItems((prev) => [
3650
- ...prev,
3651
- { kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
3652
- ]);
3653
- void agentLoop.run(rejectionMsg).catch((err) => {
3654
- const errMsg = err instanceof Error ? err.message : String(err);
3655
- log("ERROR", "error", errMsg);
3656
- setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3657
- });
3658
- } })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingRight: 1, children: [liveItems.map((item) => renderItem(item)), _jsx(StreamingArea, { isRunning: agentLoop.isRunning, streamingText: agentLoop.streamingText, streamingThinking: agentLoop.streamingThinking, thinkingMs: agentLoop.thinkingMs, planMode: planMode })] }), agentLoop.isRunning && agentLoop.activityPhase !== "idle" ? (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: agentLoop.activityPhase === "thinking" ? THINKING_BORDER_COLORS[0] : "transparent", paddingLeft: 1, paddingRight: 1, width: columns, children: _jsx(ActivityIndicator, { phase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, runStartRef: agentLoop.runStartRef, thinkingMs: agentLoop.thinkingMs, isThinking: agentLoop.isThinking, thinkingEnabled: thinkingEnabled, tokenEstimate: agentLoop.streamedTokenEstimate, charCountRef: agentLoop.charCountRef, realTokensAccumRef: agentLoop.realTokensAccumRef, userMessage: lastUserMessage, activeToolNames: agentLoop.activeToolCalls.map((tc) => tc.name), planMode: planMode, retryInfo: agentLoop.retryInfo, planDone: planSteps.filter((s) => s.completed).length, planTotal: planSteps.length, staticDisplay: true }) })) : agentLoop.stallError ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.warning, children: "⚠ API provider stream interrupted — retries exhausted." }), _jsx(Text, { color: theme.textDim, children: " Your conversation is preserved. Send a message to continue." })] })) : (doneStatus &&
3659
- !agentLoop.isRunning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.success, children: ["✻ ", doneStatus.verb, " ", formatDuration(doneStatus.durationMs)] }) }))), agentLoop.queuedCount > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.accent, children: ["⏳ ", agentLoop.queuedCount, " message", agentLoop.queuedCount > 1 ? "s" : "", " queued"] }) })), _jsx(InputArea, { onSubmit: handleSubmit, onAbort: handleAbort, disabled: agentLoop.isRunning, isActive: !taskBarFocused && !overlay, onDownAtEnd: handleFocusTaskBar, onShiftTab: handleToggleThinking, onToggleTasks: () => {
3660
- // Just flip the overlay state — Ink's log-update handles the
3661
- // live-area transition (chat input → TaskOverlay) natively, and
3662
- // the chat history above stays in scrollback. When the overlay
3663
- // closes, the history is still there (banner included).
3664
- openOverlay("tasks");
3665
- }, onToggleGoal: () => {
3666
- openOverlay("goal");
3667
- }, onToggleSkills: () => {
3668
- openOverlay("skills");
3669
- }, onTogglePixel: () => {
3670
- openOverlay("pixel");
3671
- }, onTogglePlanMode: () => {
3672
- const next = !planMode;
3673
- setPlanMode(next);
3674
- log("INFO", "plan", `Plan mode ${next ? "enabled" : "disabled"}`);
3675
- setLiveItems((items) => [
3676
- ...items,
3677
- {
3678
- kind: "plan_transition",
3679
- text: next ? "Plan Mode Activated" : "Plan Mode Deactivated",
3680
- active: next,
3681
- id: getId(),
3682
- },
3683
- ]);
3684
- }, cwd: props.cwd, commands: allCommands, eyesCount: eyesCount }), overlay === "model" ? (_jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider })) : overlay === "theme" ? (_jsx(ThemeSelector, { onSelect: handleThemeSelect, onCancel: () => setOverlay(null), currentTheme: theme.name })) : (_jsxs(_Fragment, { children: [_jsx(Footer, { model: currentModel, tokensIn: agentLoop.contextUsed, contextWindowOptions: contextWindowOptions, cwd: displayedCwd, gitBranch: gitBranch, thinkingLevel: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined, planMode: planMode, exitPending: exitPending }), !exitPending && _jsx(GoalStatusBar, { entries: goalStatusEntries })] })), (footerStatusLayout.hasBackgroundTasks ||
3685
- footerStatusLayout.hasEyesSignals ||
3686
- footerStatusLayout.hasUpdateNotice) && (_jsxs(Box, { flexDirection: footerStatusLayout.stack ? "column" : "row", width: columns, children: [footerStatusLayout.hasBackgroundTasks && (_jsx(BackgroundTasksBar, { tasks: bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, onExpand: handleTaskBarExpand, onCollapse: handleTaskBarCollapse, onKill: handleTaskKill, onExit: handleTaskBarExit, onNavigate: handleTaskNavigate, compact: footerStatusLayout.compactBackgroundTasks })), footerStatusLayout.hasEyesSignals && (_jsx(Box, { paddingLeft: footerStatusLayout.stack || bgTasks.length === 0 ? 1 : 2, paddingRight: 1, children: _jsx(Text, { color: theme.accent, bold: true, wrap: "truncate", children: `${eyesCount} eyes signal${eyesCount === 1 ? "" : "s"} · Run /eyes-improve to enhance EZ Coder` }) })), footerStatusLayout.hasUpdateNotice && (_jsx(Box, { paddingLeft: footerStatusLayout.stack ||
3687
- (!footerStatusLayout.hasBackgroundTasks && !footerStatusLayout.hasEyesSignals)
3688
- ? 1
3689
- : 2, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, wrap: "truncate", children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
3690
- }
3691
- function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
3692
- const status = enabled ? "on" : "off";
3693
- const prefix = refreshed
3694
- ? `Dynamic repo map refreshed · injection: ${status}`
3695
- : `Dynamic repo map · injection: ${status}`;
3696
- return `${prefix}\n\n${markdown}`;
2993
+ };
2994
+ const handleRejectPlan = (planPath, feedback) => {
2995
+ planOverlayPendingRef.current = false;
2996
+ const rejectionMsg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
2997
+ `Please revise the plan based on this feedback.`;
2998
+ if (props.resetUI && props.sessionStore) {
2999
+ props.sessionStore.overlay = null;
3000
+ props.sessionStore.planAutoExpand = false;
3001
+ // No wipeSession — keep history and messages so the agent picks
3002
+ // up the rejection mid-conversation.
3003
+ props.resetUI({
3004
+ pendingAction: {
3005
+ prompt: rejectionMsg,
3006
+ planEvent: { event: "rejected", detail: feedback },
3007
+ },
3008
+ });
3009
+ return;
3010
+ }
3011
+ setPlanAutoExpand(false);
3012
+ setOverlay(null);
3013
+ setDoneStatus(null);
3014
+ setLiveItems((prev) => [
3015
+ ...prev,
3016
+ { kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
3017
+ ]);
3018
+ void agentLoop.run(rejectionMsg).catch((err) => {
3019
+ const errMsg = err instanceof Error ? err.message : String(err);
3020
+ log("ERROR", "error", errMsg);
3021
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3022
+ });
3023
+ };
3024
+ const handleToggleTasks = () => {
3025
+ taskPicker.toggle();
3026
+ };
3027
+ const fullScreenOverlay = isPixelView
3028
+ ? "pixel"
3029
+ : isSkillsView
3030
+ ? "skills"
3031
+ : isPlanView
3032
+ ? "plan"
3033
+ : null;
3034
+ if (quittingSummary) {
3035
+ return (_jsx(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: _jsx(SessionSummaryDisplay, { summary: quittingSummary }) }));
3036
+ }
3037
+ return (_jsx(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: rewindCheckpoints ? (_jsx(RewindOverlay, { checkpoints: rewindCheckpoints, onRestore: handleRewindRestore, onCancel: handleRewindCancel })) : fullScreenOverlay ? (_jsx(FullScreenOverlayRouter, { overlay: fullScreenOverlay, version: props.version, cwd: props.cwd, agentRunning: agentLoop.isRunning, planAutoExpand: planAutoExpand, onClosePixel: handleCloseRemountableOverlay, onPixelFixOne: handlePixelFixOne, onPixelFixAll: handlePixelFixAll, onCloseSkills: handleCloseRemountableOverlay, onClosePlan: handleClosePlanOverlay, onApprovePlan: handleApprovePlan, onRejectPlan: handleRejectPlan })) : (_jsx(ChatScreen, { columns: columns, liveItems: uniqueItemsById(liveItems), renderItem: renderItem, isRunning: agentLoop.isRunning, visibleStreamingText: visibleStreamingText, streamingThinking: agentLoop.streamingThinking, thinkingMs: agentLoop.thinkingMs, reserveStreamingSpacing: shouldReserveStreamingSpacing, renderMarkdown: renderMarkdown, measuredLiveAreaRows: measuredLiveAreaRows, fullscreen: props.fullscreen, rows: rows, transcriptLines: transcriptLines, viewportRows: viewportRows, assistantMarginTop: shouldTopSpaceStreamingText || streamingContinuesFlushed ? 1 : 0, streamingContinuation: streamingContinuesFlushed, controlsRef: mainControlsRef, hiddenQueuedCount: hiddenQueuedCount, queueIndicatorMarginTop: shouldTopSpaceQueueIndicator ? 2 : 1, theme: theme, statusSlotVisible: statusSlotVisible, activityVisible: activityVisible, stallStatusVisible: stallStatusVisible, liveToolFeed: liveToolFeed, doneStatus: doneStatus, activityPhase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, runStartRef: agentLoop.runStartRef, isThinking: agentLoop.isThinking, thinkingLevel: thinkingLevel, tokenEstimate: agentLoop.streamedTokenEstimate, charCountRef: agentLoop.charCountRef, realTokensAccumRef: agentLoop.realTokensAccumRef, lastUserMessage: lastUserMessage, activeToolNames: agentLoop.activeToolCalls.map((tc) => tc.name), retryInfo: agentLoop.retryInfo, planDone: planSteps.filter((s) => s.completed).length, planTotal: planSteps.length, formatDuration: formatDuration, inputControls: {
3038
+ onSubmit: handleSubmit,
3039
+ onAbort: handleAbort,
3040
+ injectText: composerInject,
3041
+ inputActive: !taskBarFocused && !overlay,
3042
+ onDownAtEnd: handleFocusTaskBar,
3043
+ onShiftTab: handleToggleThinking,
3044
+ onToggleTasks: handleToggleTasks,
3045
+ onToggleSkills: () => openOverlay("skills"),
3046
+ onTogglePixel: () => openOverlay("pixel"),
3047
+ onToggleGoal: () => openOverlay("goal"),
3048
+ onToggleMarkdown: () => setRenderMarkdown((prev) => !prev),
3049
+ cwd: props.cwd,
3050
+ commands: allCommands,
3051
+ mouseScroll: props.fullscreen,
3052
+ onScroll: scrollTranscriptByLines,
3053
+ }, taskPicker: {
3054
+ open: taskPicker.open,
3055
+ tasks: taskPicker.tasks,
3056
+ onClose: taskPicker.close,
3057
+ onStart: taskPicker.start,
3058
+ onRunAll: taskPicker.runAll,
3059
+ onDelete: taskPicker.deleteTask,
3060
+ }, goalPane: {
3061
+ cwd: props.cwd,
3062
+ autoExpandNewest: goalAutoExpand,
3063
+ statusEntries: goalStatusEntries,
3064
+ onClose: handleCloseGoalOverlay,
3065
+ onRunGoal: handleRunGoal,
3066
+ onVerifyGoal: handleVerifyGoal,
3067
+ onPauseGoal: handlePauseGoal,
3068
+ onRefineGoal: handleRefineGoal,
3069
+ }, overlay: overlay, onModelSelect: handleModelSelect, onModelCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider, onThemeSelect: handleThemeSelect, onThemeCancel: () => setOverlay(null), currentTheme: theme.name, contextUsed: agentLoop.contextUsed, contextWindowOptions: contextWindowOptions, displayedCwd: displayedCwd, gitBranch: gitBranch, planMode: planMode, exitPending: exitPending, footerStatusLayout: footerStatusLayout, backgroundTasks: bgTasks, taskBarFocused: taskBarFocused, taskBarExpanded: taskBarExpanded, selectedTaskIndex: selectedTaskIndex, onTaskBarExpand: handleTaskBarExpand, onTaskBarCollapse: handleTaskBarCollapse, onTaskKill: handleTaskKill, onTaskBarExit: handleTaskBarExit, onTaskNavigate: handleTaskNavigate })) }));
3697
3070
  }
3698
3071
  //# sourceMappingURL=App.js.map